Josh Lee
Posted on May 31, 2021
Previously, we set up our Ruby on Rails app to use React.
Now, we need to do a few more things to make sure our app is really functional. We still have to
Set up our model in rails
Have our frontend connect to our backend
Integrate Redux so React works better.
Let’s get started.
Setting up our Post model and controller in rails
This is going to be pretty common Rails code. First, create the model in “app/models/Post.rb”.
class Post < ApplicationRecord
end
Next, we’re going to set up our serializer. This basically turns our model into JSON that we can send to our frontend. Create “app/serializers/post_serializer.rb” and put the following:
class PostSerializer
include FastJsonapi::ObjectSerializer
attributes :title, :body
end
The attributes are attributes on our model that we’re going to expose as JSON. This reminds me, we need to add the FastJsonapi gem. Go to your gemfile and add:
gem 'fast_jsonapi'
Run bundle install.
Now we need to set up our model in the database. Run the following:
rails g migration create_posts
And in the migration file:
class CreatePosts < ActiveRecord::Migration[6.1]
def change
create_table :posts do |t|
t.string :title
t.string :body
t.timestamps
end
end
end
Then run the migration:
rails db:migrate
Now, on to the controller. Set up your controller code in
“app/controller/api/v1/posts_controller.rb”. This is common to your usual Rails CRUD controller code, but we’re going to be rendering JSON instead of rendering views or redirecting.
Here’s the code for the controller:
module Api
module V1
class PostsController < ApplicationController
def index
posts = Post.all
render json: PostSerializer.new(posts).serialized_json
end
def show
post = Post.find(params[:id])
render json: PostSerializer.new(post).serialized_json
end
def create
post = Post.new(post_params)
if post.save
render json: PostSerializer.new(post).serialized_json
else
render json: {error: post.errors.messsages}
end
end
def update
post = Post.find(params[:id])
if post.update(post_params)
render json: PostSerializer.new(post).serialized_json
else
render json: { error: post.errors.messages }
end
end
def destroy
post = Post.find(params[:id])
if post.destroy
head :no_content
else
render json: { error: post.errors.messages }
end
end
private
def post_params
params.require(:post).permit(:title, :body)
end
end
end
Now’s a good time to test all of these actions with something like Postman. Go ahead and test out your API before moving on to the front end.
We’re going to write a lot of code in the upcoming sections to connect to our backend. It’s important that your backend is working properly.
Open Rails console and add a few records so we can see our data. Here’s what I did.
Post.create(title: "one", body:"something")
Post.create(title: "two", body:"something else")
Now you should be getting some records back when you hit your index endpoint for your posts.
Adding Redux to Ruby on Rails
Create a folder and folder “app/javascript/src/api/api.js” This is what we’re going to use to talk to our back end. Here’s what our file is going to look like:
import axios from 'axios'
const ROOT_PATH = '/api/v1'
const POSTS_PATH = `${ROOT_PATH}/posts`
export const getPosts = () => {
return axios.get(POSTS_PATH)
}
We’re importing axios so we can make http requests to our backend. Then, we’re setting up some constants for our routes. Finally, we’re creating a function that makes a get request to our posts route.
Add axios using yarn:
yarn add axios
Now’s the time to add redux. I’m going to try to explain the best I can, but I assume you have some knowledge of how redux works before you start trying to add redux to Rails.
Create an actions folder in “app/javascript/src/actions” and create a posts.js file in that folder. In that file put this:
import * as api from '../api/api'
export const getPosts = () => async (dispatch) => {
const { data } = await api.getPosts()
}
We’re importing our api so we can use the methods there. We are also creating a function that just calls our api and returns the data. The “dispatch” section might look strange, but we’re doing that so redux-thunk works.
We’re going to come back to this function later, but this is enough to test it out.
EDIT: We’re not going to test out this function before we add to it. Sit tight and we’ll come back to this function.
Go to your index.jsx file at “app/javascript/packs/index.jsx” and make the file look like this
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter as Router, Route } from 'react-router-dom'
import App from '../src/components/App'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import reducers from '../src/reducers'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(reducers, composeEnhancers(applyMiddleware(thunk)))
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(
<Provider store={store}>
<Router>
<Route path="/" component={App}/>
</Router>
</Provider>,
document.body.appendChild(document.createElement('div')),
)
})
So what’s going on with all this code? Well, first we are importing everything we need from react-redux and redux-thunk here:
import { Provider } from 'react-redux'
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import reducers from '../src/reducers'
We’re also importing a reducers file that we will create in a second.
Then, this line is setting up Redux so we can work with the Chrome redux dev tools. If you don’t have this set up, the Chrome extension won’t work:
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
Next, we’re creating our store that lets us work with state. We’re also telling our app we want to use redux-thunk.
const store = createStore(reducers, composeEnhancers(applyMiddleware(thunk)))
Finally, we’re wrapping our app in the Provider tag. This has to do with accessing the store or state in our app.
<Provider store={store}>
<Router>
<Route path="/" component={App}/>
</Router>
</Provider>
That’s it for that file. Now we need to create that reducer we just imported. But first, make sure you add the packages using yarn.
yarn add react-redux redux-thunk
Create a reducers folder in “app/javascript/src” and create two files. Create a “posts.js” file and an “index.js” file. Let’s open the “posts.js” file first.
This file is going to keep track of the posts on your apps state. This file’s job is to update all the posts when certain actions are dispatched from your actions files.
This is what the file looks like:
import { GET_POSTS } from '../types/index'
export default (posts = [], action ) => {
switch (action.type) {
case GET_POSTS:
return action.payload
default:
return posts
}
}
Let’s break down what’s happening here. First, we’re importing a GET_POSTS type. We’ll create that in a second.
Next, we’re exporting a function and setting the initial state of posts to an empty array. Then we have the switch statement.
switch (action.type) {
case GET_POSTS:
return action.payload
default:
return posts
}
What this is doing is saying “Whenever I see the GET_POSTS action, I’m going to take the payload from that action and set my posts equal to that payload. For all other actions (default), I’m just going to return the posts and do nothing.
Later, when we use our actions, we will send types like GET_POSTS that tell this reducer to use the data we pass it. If any other action types are passed to it, it won’t do anything.
Before we forget, let’s create that types folder and file in “app/javascript/src/types/index.js”. This will help us later on if we mistype any of our types.
export const GET_POSTS = "GET_POSTS"
Now we go to our “app/javascript/src/reducers.index.js” file. This file just combines all your reducers.
import { combineReducers } from 'redux'
import posts from './posts'
export default combineReducers({
posts: posts
})
What this does is tells redux that we want a key on our state called “posts” and set that equal to the posts in our state.
Now that we have our reducers set up, we can go back to our action creator file and dispatch actions. Basically, this lets our actions talk to our reducers. Back in “apps/javascript/src/actions/posts.js” make your file look like this.
import * as api from '../api/api'
import { GET_POSTS } from '../types/index'
export const getPosts = () => async (dispatch) => {
const { data } = await api.getPosts()
dispatch({
type: GET_POSTS,
payload: data.data
})
}
Here’s what we’re doing here. We are using our api to get data from our rails backend. Then, with “dispatch” we are telling all of our reducers “hey, if you are subscribed to the GET_POSTS action, I have some data for you.”
We only have one reducer right now, but all of the reducers would look at this action and the only ones that are subscribed to GET_POSTS will actually do anything. In our case, our posts reducer is looking out for this action type. It’s going to see the data in the payload and then set that in our posts key on our state.
Now let’s actually use all of this code we set up!
Back in our Posts component at “app/javascript/src/components/Posts/Posts” write the following.
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { getPosts } from '../../actions/posts'
const Posts = () => {
const dispatch = useDispatch()
const posts = useSelector(state => state.posts)
useEffect(() => {
dispatch(getPosts())
}, [])
if (posts.length === 0) { return <div>loading...</div>}
console.log(posts)
return (
<div>
<h1>Posts</h1>
<p>This is our posts page.</p>
</div>
)
}
export default Posts
What’s going on here?
We are getting some functions from react-redux and getting our action creator function.
import { useDispatch, useSelector } from 'react-redux'
import { getPosts } from '../../actions/posts'
We’re setting up our dispatch function here.
const dispatch = useDispatch()
Next, we’re telling react to create a variable called posts and set it equal to the posts in the redux store.
const posts = useSelector(state => state.posts)
Now, we’re saying “when this component loads, go get all the posts using my action creator.
useEffect(() => {
dispatch(getPosts())
}, [])
If our page loads before our data comes back, we’re going to have a loading signal. Otherwise, if you start trying to access your data before if comes back from the server, your app will crash.
if (posts.length === 0) { return <div>loading...</div>}
Then, we’re just console.loging our posts. You should be able to see them in the Chrome redux dev tools, too.
console.log(posts)
Awesome, now our react app can read data from redux store, data that is from our backend. We’re at the home stretch!
We don’t just want to console.log our data though. So, let’s fix that. In our return function, we going to put another function like so.
return (
<div>
<h1>Posts</h1>
{renderPosts()}
</div>
}
Let’s make a function in this same file called renderPosts. Here, we’re going to loop through each of our posts and render a component.
const renderPosts = () => {
return posts.map(post => {
return <PostListItem key={post.id} post={post} />
})
}
We’re passing the current post to each item. We’re also giving it a key, otherwise react will yell at us and it will hurt performance.
Import the list item at the top.
import PostListItem from './PostListItem'
Then create it at “app/javascript/src/components/Post/PostListItem”.
import React from 'react'
const PostListItem = ({post}) => {
return(
<div>
<h2>{post.attributes.title}</h2>
<p>{post.attributes.body}</p>
</div>
)
}
export default PostListItem
You should now see all of your posts.
In the next article, I'll cover CRUD operations in Rails and React. Stay tuned!
If you want to learn more about web development, make sure to follow me on Twitter.
Posted on May 31, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 27, 2024