Rails + React JS application with CRUD operation

Setting up Rails application with ReactJS usually takes some time. Setting up React Router and Redux for Front-end is an essential part of using React with Ruby on Rails.

This tutorial will help you setup Ruby on Rails application with ReactJS. It will also demonstrate how to setup React Routing and React components to perform basic Create(C), Read(R), Update(U) and Delete(D) operation.

If you are someone, who works with multiple projects and create new projects every now and then, it is troubling to spend time on setting up Ruby on Rails application with ReactJS.

Step 1: Install Ruby and Rails versions

To setup a new Rails 6 application, you need to be on Ruby 2.5.0+. To install Ruby on Rails with RVM, use the command given below. We will use current stable ruby version 2.6.5

rvm install 2.6.5

Once Ruby 2.6.5 is installed, install gem for rails 6.0.0 as given below.

gem install rails -v 6.0.0

Verify that you have correct Rails version installed with the command given below.

rails --version
# Rails 6.0.0
Step 2: Create a new Rails project

Create a new Rails project with the command given below.

rails new rails-react-js-ssr-setup --database=postgresql --webpack=react

This will create a new Rails 6 application and install necessary gems required for Rails 6 application.

  • --database option helps select database to use. We will use postgresql as database and
  • --webpack option is used to select javascript framework. We will use react.

Webpack react makes sure package.json has React dependencies added.

Change directory to newly created Rails application directory.

cd rails-react-js-ssr-setup

Run database setup Rails command to get the repository to work with backend.

bundle exec rails db:create db:migrate

Let’s commit this repository in VCS (Git) as initial repository code.

git add .
git commit -m "Initial setup."
Step 3: Setup React with react_on_rails gem

Now that we have an inital setup ready with Ruby on Rails 6, let’s setup React with React on Rails gem. We will use latest stable version 11.3.0

Add gem to Gemfile with command given below.

# React setup with Ruby on Rails
gem 'react_on_rails', '~> 11.3.0'

Perform bundle install to install gem as given below.

bundle install

Once this is done, we need to run react on rails initializers as given below.

rails generate react_on_rails:install

Perform bundle install after step above, since it adds gem mini_racer to Gemfile and React files for Hello World setup.

bundle install
Step 4: Run Rails server: Hello World

React on Rails comes with Procfile.dev and Procfile.dev-server file. Thus, we need to install foreman gem to run multiple processes. In this case, there are two processes.

  • Rails Server
  • Webpack server

Procfile.dev-server is given below for the reference.

# You can run these commands in separate shells instead of using foreman
web: rails s -p 3000

# Next line runs the webpack-dev-server
# You can edit config/webpacker.yml to set HMR to true to see hot reloading.
# Note, hot and live reloading don't work with the default generator setup on top of
# the rails/webpacker Webpack config with server rendering.
# If you have server rendering enabled, modify the call to bin/webpack-dev-server line
# so you add `--inline=false` and then CSS is not inlined.
# Otherwise, you will have an error. If you want HMR and Server Rendering, see
# the example in the https://github.com/shakacode/react-webpack-rails-tutorial
client: sh -c 'rm -rf public/packs/* || true && bundle exec rake react_on_rails:locale && bin/webpack-dev-server'

Run the server to view hello world component built in React.

foreman start -f Procfile.dev-server

Visit http://localhost:3000/hello_world to see Hello world in React on web.


Step 5: Setup React Router

To setup React Routing, we need to install react-router and react-router-dom package.

Run the command given below.

yarn add react-router react-router-dom

The above command will install react-router and react-router-dom package and add it to package.json file. This will also lock dependencies in yarn.lock file.

Now, let’s create another rails route to demonstrate react routing.

get 'bye_world', to: 'hello_world#index'

We will also add a root route that points to same hello_world#index controller action as given below.

root 'hello_world#index'

Entire config/routes.rb file looks as given below.

# config/routes.rb
Rails.application.routes.draw do
  root 'hello_world#index'
  get 'hello_world', to: 'hello_world#index'
  get 'bye_world', to: 'hello_world#index'
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

We have three routes in total.

  • Root Route - Points to hello_world#index controller action
  • Hello World Route - Points to hello_world#index controller action
  • Bye World Route - Points to hello_world#index controller action

As we can see, all there are two routes pointing to same controller#action. This is delebrately done to show how react routing works.

Now, let’s add react route links.

Create a file App.js and Routes.js in app/javascript directory.

touch app/javascript/App.js app/javascript/Routes.js

Let’s add a couple of routes to front-end as described in backend config/routes.rb file.

// app/javascript/Routes.js
import React from 'react';
import {
  Switch,
  Route,
} from "react-router-dom";
import HelloWorld from './bundles/HelloWorld/components/HelloWorld';

export default () => {
  return (
    <Switch>
      <Route exact path="/">
        <h3>Root Path Component</h3>
      </Route>
      <Route path="/hello_world">
        <h3>Hello World Component</h3>
      </Route>
      <Route path="/bye_world">
        <h3>Bye World Component</h3>
      </Route>
    </Switch>
  );
}

Let’s mount these routes under Router as given below in App.js.

// app/javascript/App.js
import React from 'react';
import {
  BrowserRouter as Router,
  Link
} from "react-router-dom";
import Routes from './Routes';

export default class App extends React.Component {
  render() {
    console.log('app being mounted');
    return (
      <Router>
        <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/hello_world">Hello World</Link>
          </li>
          <li>
            <Link to="/bye_world">Bye World</Link>
          </li>
        </ul>
        <hr />
        </div>
        <Routes />
      </Router>
    );
  }
}

Restart the foreman server to see React Routing in action. We have three routes in total.

  • Root Path - It shows list of routes as unordered list
  • Hello World Route Path - It shows Hello World Component
  • Bye World Route Path - It shows Bye World Component

Step 6: CRUD operation - Rails APIs

Now, we will demonstrate Create (C), Read (R), Update(U) and Delete (D) operations. Let’s create an ActiveRecord model Post to begin with.

rails generate model Post title:string description:text is_published:boolean

It will create files given below.

Running via Spring preloader in process 77236
      invoke  active_record
      create    db/migrate/20191112103934_create_posts.rb
      create    app/models/post.rb
      invoke    test_unit
      create      test/models/post_test.rb
      create      test/fixtures/posts.yml

Let’s add a few entries to posts via seed.

# db/seeds.rb
SAMPLE_POSTS = [{
  title: 'Rails Routes expanded view option',
  description: 'Rails 6 adds support to show rails routes in an expanded format with --expanded option.',
  is_published: true
},{
  title: 'Rails find_in_batches vs find_each',
  description: 'This article discusses how we can use find_in_batches and find_each to query records in batches with ActiveRecord.',
  is_published: true
}, {
  title: 'Rails Routes member vs collection',
  description: 'Member routes act on a member of the resource. Collection routes acts on resources in general.',
  is_published: true
}]

SAMPLE_POSTS.each do |post|
  Post.create(post)
end

Run database migration and seed rake scripts to create posts database and populate seed data respectively.

bundle exec rails db:migrate db:seed

Now that we have model and records for posts in database, let’s define routes and controller to perform CRUD operation on posts entity.

# app/controllers/api/v1/posts_controller.rb
class Api::V1::PostsController < ApplicationController
  def index
    all_posts = Post.all
    render json: all_posts
  end

  def create
    post = Post.create(post_params)
    render json: post
  end

  def show
    post = Post.find(params[:id])
    render json: post
  end

  def update
    post = Post.find(params[:id])
    post.update(post_params)
    render json: post
  end

  def destroy
    Post.destroy(params[:id])
    head :ok
  end

  private

  def post_params
    params.permit(:title, :description, :is_published)
  end
end

Let’s add routes to access Create, Read, Update and Delete operations externally.

# config/routes.rb
namespace :api do
  namespace :v1 do
    resources :posts, only: [:index, :create, :show, :update, :destroy]
  end
end

We have used resource to create routes and have specified exact four routes that we want via only option. Please find why we have used resource and not resources

To make sure, we have routes correctly set up and they return response for posts resource, let’s send a cURL Request to fetch a list of posts.

curl http://localhost:3000/api/v1/posts

This command returns reponse as given below.

[{
	"id": 1,
	"title": "Rails Routes expanded view option",
	"description": "Rails 6 adds support to show rails routes in an expanded format with --expanded option.",
	"is_published": true,
	"created_at": "2019-11-12T10:48:14.231Z",
	"updated_at": "2019-11-12T10:48:14.231Z"
}, {
	"id": 2,
	"title": "Rails find_in_batches vs find_each",
	"description": "This article discusses how we can use find_in_batches and find_each to query records in batches with ActiveRecord.",
	"is_published": true,
	"created_at": "2019-11-12T10:48:14.242Z",
	"updated_at": "2019-11-12T10:48:14.242Z"
}, {
	"id": 3,
	"title": "Rails Routes member vs collection",
	"description": "Member routes act on a member of the resource. Collection routes acts on resources in general.",
	"is_published": true,
	"created_at": "2019-11-12T10:48:14.261Z",
	"updated_at": "2019-11-12T10:48:14.261Z"
}]

We are ready with backend APIs. Let’s create front-end components to use these backend APIs and perform CRUD operation.

Step 7: CRUD operation - React components

First, we will create a component to list down all the available posts. Create a new directory under app/javascript/bundles with name posts to create React files for posts entity.

Component to list Posts
// app/javascript/bundles/posts/index.js
import React from 'react';

export default class PostsList extends React.Component {
  constructor(props) {
    super(props);
    this.state = { posts: [] };
  }

  componentDidMount() {
    fetch('/api/v1/posts').
      then((response) => response.json()).
      then((posts) =>  this.setState({ posts }));
  }

  render() {
    const { posts } = this.state;
    return (
      <div>
        <h3>All Posts</h3>
        <table>
          <thead>
            <tr>
              <th>ID</th>
              <th>Title</th>
              <th>Description</th>
              <th>Is Published</th>
            </tr>
          </thead>
          <tbody>
          {
            posts.map((post) => {
              return (
                <tr key={post.id}>
                  <td>{post.id}</td>
                  <td>
                    <Link to={`/posts/${post.id}`}>
                      {post.title}
                    </Link>
                  </td>
                  <td>{post.description}</td>
                  <td>{post.is_published ? 'Yes' : 'No' }</td>
                </tr>
              )
            })
          }
          </tbody>
        </table>
      </div>
    );
  }
}

This component does following things:

  • Initializes posts to an empty array in the constructor
  • Fetches posts to list from backend API that created in step above.
  • Render method iterates over posts from React state and displays them in a table format.

Let’s change React route to open Posts listing component at the root path.

// app/javascript/routes.js
import React from 'react';
import {
  Switch,
  Route,
} from "react-router-dom";
import HelloWorld from './bundles/HelloWorld/components/HelloWorld';
import Posts from './bundles/posts/index';

export default () => {
  return (
    <Switch>
      <Route exact path="/">
        <Posts />
      </Route>
      <Route
        path="/posts/:id"
        exact
        component={PostDetails}
       />
    </Switch>
  );
}
Component to view a Post

We have a list of Posts displayed at the root path of the application. Let us create a new React component to display an individual post.

// app/javascript/bundles/posts/PostDetails.js
import React from 'react';

export default class PostDetails extends React.Component {
  constructor(props) {
    super(props);
    this.state = { post: {} };
  }

  componentDidMount() {
    const { match: { params: { id } } } = this.props;
    fetch(`/api/v1/posts/${id}`).
      then((response) => response.json()).
      then((post) => this.setState({ post }));
  }

  render() {
    const { post } = this.state;
    return (
      <div>
        <div>
          <label> Title </label>
          <p> {post.title} </p>
        </div>

        <div>
          <label> Description </label>
          <p> {post.description} </p>
        </div>

        <div>
          <label> Is Published </label>
          <p> {post.is_published} </p>
        </div>
      </div>
    );
  }
}

This component does following things.

  • Initialize post as an empty object in a constructor
  • Fetch the post in question in componentDidMount lifecycle event based on post ID received in props
  • Render method returns HTML that prints title, description and is_published values

Create a new Route for the post details page as given below in config/routes.rb for backend and in Routes.js for React front-end.

  get 'posts/:id', to: 'hello_world#index';

Entire config/routes.rb looks as given below.

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :posts, only: [:index, :show, :update, :destroy]
    end
  end
  get 'posts/:id', to: 'hello_world#index';
  root 'hello_world#index'
end

We have removed hello_world and bye_world routes now.

For react routes, add in app/javascript/Routes.js.

import PostDetails from './bundles/posts/PostDetails';
//.
//.
<Route path="posts/:id" component={PostDetails}>

Entire routes files looks as given below.

// app/javascript/routes.js
import React from 'react';
import {
  Switch,
  Route,
} from "react-router-dom";
import HelloWorld from './bundles/HelloWorld/components/HelloWorld';
import Posts from './bundles/posts/index';
import PostDetails from './bundles/posts/PostDetails';

export default () => {
  return (
    <Switch>
      <Route exact path="/">
        <Posts />
      </Route>
      <Route
        path="/posts/:id"
        exact
        component={PostDetails}
       />
    </Switch>
  );
}

We have removed hello_world and bye_world routes now.

Component to delete a post

To perform put, patch, delete requests from API, we need to add following code to app/controllers/application_controller.rb

# app/controllers/application_controller
class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session
end

Let’s add a link to delete a post from Posts list page. We will add two things.

  • Extra column in a table to show delete action
  • handleDelete callback that will send an ajax request to delete a record

The posts list page component is given below after the change mentioned above.

// app/javascript/bundles/posts/index.js
import React from 'react';
import { Link } from 'react-router-dom';

export default class PostsList extends React.Component {
  constructor(props) {
    super(props);
    this.state = { posts: [] };
  }

  componentDidMount() {
    this.fetchPostsList();
  }

  fetchPostsList = () => {
    fetch('/api/v1/posts').
      then((response) => response.json()).
      then((posts) =>  this.setState({ posts }));
  };

  handleDelete = (postId) => {
    fetch(`/api/v1/posts/${postId}`, { method: 'delete' }).
      then((response) => {
        alert('Post deleted successfully')
        this.fetchPostsList();
      });
  }

  render() {
    const { posts } = this.state;
    return (
      <div>
        <h3>All Posts</h3>
        <table>
          <thead>
            <tr>
              <th>ID</th>
              <th>Title</th>
              <th>Description</th>
              <th>Is Published</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
          {
            posts.map((post) => {
              return (
                <tr key={post.id}>
                  <td>{post.id}</td>
                  <td>
                    <Link to={`/posts/${post.id}`}>
                      {post.title}
                    </Link>
                  </td>
                  <td>{post.description}</td>
                  <td>{post.is_published ? 'Yes' : 'No' }</td>
                  <td>
                    <button onClick={() => this.handleDelete(post.id) }>
                      Delete
                    </button>
                  </td>
                </tr>
              )
            })
          }
          </tbody>
        </table>
      </div>
    );
  }
}

This component does things mentioned below.

  • Adds a handleDelete callback which send delete post ajax request
  • Javascript alert is shown after post is deleted.
Component to Create a new post

To create a new post, we need a React component that shows inputs for title, description and is_published.

We have already added backend route in config/routes.rb. Let us add React Route to create a post.

<Route
  path="/posts/new"
  exact
  component={CreatePost}
  />

Place it before /posts/:id route, so that it gets matched first.

Now, create a component that will do following things.

  • Show inputs for title, description and is published fields
  • Maintain input values in a state
  • Submit values to create post API to create a post
  • Redirect to root path to see newly added post on posts list
// app/javascript/bundles/posts/CreatePost.js
import React from 'react';
import {Redirect} from 'react-router-dom';

export default class CreatePost extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      title: '',
      description: '',
      is_published: true
    }
  }

  handleInputChange = (event) => {
    this.setState({ [event.target.name]: event.target.value });
  }

  createPostRequest = (event) => {
    console.log('this.state', this.state);
    fetch('/api/v1/posts', {
      method: 'post',
      body: JSON.stringify(this.state),
      headers: { 'Content-Type': 'application/json' },
    }).then((response) => {
      alert('Post created successfully');
      location.href = '/';
    });
  }

  render() {
    const {title, description, is_published} = this.state;
    return (
      <div>
        <h3>New Post</h3>
        <div>
          <label>Title: </label>
          <input
            type='text'
            name='title'
            value={title}
            onChange={this.handleInputChange}
            />
        </div>
        <div>
          <label>Description: </label>
          <input
            type='text'
            name='description'
            value={description}
            onChange={this.handleInputChange}
            />
        </div>
        <div>
          <label>Is Published: </label>
          <input
            type='text'
            name='is_published'
            value={is_published}
            onChange={this.handleInputChange}
            />
        </div>
        <button onClick={this.createPostRequest}>Create</button>
      </div>
    );
  }
}
Component to update a post

Add a Rails routes to show edit post component as given below.

// config/routes.rb
get 'posts/:id/edit', to: 'hello_world#index';

Let’s add a React route to edit a post as given below.

// In app/javascript/Routes.js
<Route
  path="/posts/:id/edit"
  exact
  component={UpdatePost}
  />

Add a link to UpdatePost component from posts list component.

// app/javascript/bundles/post/index.js
<Link to={`/posts/${post.id}/edit`}>
  Edit
</Link>

Let’s create a component to update existing post.

  • Component shows existing values when component is mounted
  • It maintains any changes to values for the attributes on a post
  • Sends an update request to update post on click of a submit button
// app/javascript/bundles/posts/UpdatePost.js
import React from 'react';

export default class UpdatePost extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      title: '',
      description: '',
      is_published: true
    }
  }

  componentDidMount() {
    const { match: { params: { id } } } = this.props;
    fetch(`/api/v1/posts/${id}`).
      then((response) => response.json()).
      then((post) => this.setState({ ...post }));
  }

  handleInputChange = (event) => {
    this.setState({ [event.target.name]: event.target.value });
  }

  updatePostRequest = (event) => {
    fetch(`/api/v1/posts/${this.state.id}`, {
      method: 'put',
      body: JSON.stringify(this.state),
      headers: { 'Content-Type': 'application/json' },
    }).then((response) => {
      alert('Post updated successfully');
      location.href = '/';
    });
  }

  render() {
    const {title, description, is_published} = this.state;
    return (
      <div>
        <h3>New Post</h3>
        <div>
          <label>Title: </label>
          <input
            type='text'
            name='title'
            value={title}
            onChange={this.handleInputChange}
            />
        </div>
        <div>
          <label>Description: </label>
          <input
            type='text'
            name='description'
            value={description}
            onChange={this.handleInputChange}
            />
        </div>
        <div>
          <label>Is Published: </label>
          <input
            type='text'
            name='is_published'
            value={is_published}
            onChange={this.handleInputChange}
            />
        </div>
        <button onClick={this.updatePostRequest}>Update</button>
      </div>
    );
  }
}

Source Code

You can find source code of the repository created illustrating everything from this blog post.

Ruby on Rails 6 + ReactJS CRUD Application

References