DEV Community

Josh Lee
Josh Lee

Posted on • Updated on

How to set up React.js with a Ruby on Rails Project Part 2 – Redux

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

Run bundle install.

Now we need to set up our model in the database. Run the following:

rails g migration create_posts
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then run the migration:

rails db:migrate
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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')),
  )
})
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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
  })
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

We’re setting up our dispatch function here.

const dispatch = useDispatch()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Now, we’re saying “when this component loads, go get all the posts using my action creator.

useEffect(() => {
     dispatch(getPosts())
}, [])
Enter fullscreen mode Exit fullscreen mode

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>}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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} />
    })
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)