Introduction

This is part three of a four part tutorial on building a Create-Read-Update-Delete (CRUD) application with Ruby on Rails and React. The first two tutorials treated the Rails API and the React client as separate applications running on separate servers. That is a good setup for a Single Page Application. But what if you want to mix the standard Rails Views with some React views all running from the same domain? A good option is to integrate React into your Rails app with the Webpacker gem.

Webpack is an open-source JavaScript pre-processor and module bundler used in conjunction with front-end libraries like React, AngularJS and View.js. It bundles all the .jsx, .sass, .hbs, etc. code into normal .js and .css that browsers understand after it is pre-compiled. It comes with webpack-dev-server which enables hot reloading in the development environment, meaning the browser is automatically refreshed every time you save a file. The create-react-app generator we used in Part 2 of this tutorial incorporates Webpack.

The webpacker gem was created by the Rails team. It is a middleware utility that lets Webpack work within a Rails application. Starting with Rails 5.1, you can automatically enable the Webpacker gem when you create a new application. And you can specify to integrate it specifically for React (or Angular, Vue, Elm, Stimulus).

In this tutorial we will duplicate the Articles CRUD application we created in Parts 1 and 2. It will be the same except for the following differences:

  • React will be integrated into our Rails application using the Webpacker gem.
  • We will serialize our API with the active_model_serializer gem instead of the jbuilder gem.
  • We will add an api namespace to our Rails articles routes.
  • We don't need to install the rack-cors gem since React and Rails will be running from the same origin.
  • We will use React Router's HashRouter instead of BrowserRouter to avoid conflict with the Rails Router.
  • We will use the ES6 native Fetch API rather than React's Axios module for AJAX requests.

  • Setup the Application

    The finished code for this tutorial can be found on Github

    Generate the Application

    Let's create a new application enabled with Webpack and React. You need to have node and yarn installed on your computer.

  • mkdir rails-with-webpacker-react; cd rails-with-webpacker-react
  • If you are using RVM for Ruby and Gems version control you can set your gemset at this point.
  • rails new . --webpack=react

    Or Add Webpacker to an Existing Rails Application

    If you wanted to add webpacker to an existing app, add the webpacker gem:
    # Gemfile
    gem 'webpacker'
    
  • bundle install
  • Then run the webpacker generator:

    rails webpacker:install:react

    Add the Active Model Serializers gem

    We could use the JBuilder gem to serialize objects to and from JSON strings like we did in Part 1 of this tutorial. This time though, we will use the Active Model Serializers gem discussed in Part 1. Once installed, Rails will automatically generate a serializer file for us when we generate a scaffold or resourse. Although, if we generate just the model, then we have to generate the serializer separately.
    # Gemfile
    gem 'active_model_serializers'
    

    bundle install

    Create the Articles resource

    We'll create an Articles model, database table, routes with an api namespace, and controller serving only JSON. It will have just two attributes, title and content, besides the ID and timestamps that Rails generates. There will be no Rails views for this resourse. We'll handle that with React.

  • rails generate model Article title:string content:text
  • rails db:migrate
  • rails generate serializer article title content
  • The last command will generate our serializer for us. It will look like this:

    # app/serializers/article_serializer.rb
    class ArticleSerializer < ActiveModel::Serializer
      attributes :id, :title, :content
    end
    

    Add some seed data:

    # db/seed.rb
    articles = Article.create([
      {title: "Learn Ruby", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."},
      {title: "Learn Rails", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."},
      {title: "Learn React", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}
    ])
    

    rails db:seed

    Add routes with an "api" namespace.

    # config/routes.rb 
    Rails.application.routes.draw do
      namespace :api do
        resources :articles
      end
    end
    

    Create a controller file then populate it.

  • mkdir app/controllers/api; touch app/controllers/api/articles_controller.rb
  • # app/controllers/api/articles_controller.rb
    class Api::ArticlesController < ApplicationController
      skip_before_action :verify_authenticity_token
    
      def index
        @articles = Article.all
        render json: @articles
      end
    
      def show
        @article = Article.find(params[:id])
        render json: @article
      end
    
      def create
        @article = Article.new(article_params)
        if @article.save
          render json: @article, status: :created
        else
          render json: @article.errors, status: :unprocessable_entity
        end
      end
    
      def update
        @article = Article.find(params[:id])
        if @article.update(article_params)
          render json: @article, status: :ok
        else
          render json: @article.errors, status: :unprocessable_entity
        end
      end
    
      def destroy
        @article = Article.find(params[:id])
        @article.destroy
        head :no_content
      end
      
      private
        def article_params
          params.require(:article).permit(:title, :content)
        end
    end
    

    Okay, our Rails API should be working. You can test it out by starting the server.

    rails server

    Then go to http://localhost:3000/api/articles. You should see the seeded articles in JSON format. You don't need to add .json to the URL (but you can) since in our controller actions we specify json format in our render. Now stop the server because we have another server solution.

    Foreman

    We just ran the Rails server like we always have. But when we are using Webpack we need to run two servers. But wait a minute. I thought creating an integrated app would mean we only need to run one server since it's all on one domain? Well, one server will run both our Rails API and our Rails views. And you'll see shortly that the React components are being inserted into a Rails view file. So yes, the Rails API and React served pages will be running on the same server and to the same domain (localhost:3000 in our dev environment). However, we do need to run a separate server for the Webpack build process and to have live code reloading in our browser during development. Webpack dev server runs on localhost:3035 but won't display a view to that port.

    We can use the foreman gem to start both servers with a single command. Foreman is a utility you call from the Terminal so you save it directly to your system, not though your Gemfile.

    gem install foreman

    Then create a Procfile for your development environment and populate it with the below.

  • touch Procfile.dev
  • # Procfile.dev
    web: bundle exec rails s
    webpacker: ./bin/webpack-dev-server
    

    Now run the server with with foreman start -procfile.dev which defaults to port 5000. If you want to run it on port 3000 then run:

    foreman start -f Procfile.dev -p 3000

    If you go back to http://localhost:3000/api/articles in your browser and refresh it you should still see the JSON rendering of our articles.

    Pages Controller

    We need a Rails view to insert the React components. This is where our integrated app differs from the separated Rails and React applications we created in Parts 1 and 2. The React views will be served by the Rails server through a Rails view page (or multiple Rails view pages if you have different React views for different resources). In our case we'll make it simple and just have React interact with one Rails view page. Let's generate the controller with a single action.

    rails g controller pages index --no-assets --no-helper

    Make that action the root route.

    # config/routes.rb 
    root to: 'pages#index'
    
    namespace :api do
      resources :blogs
    end
    

    Populate the Rails view file with a single div element with id of "root." And add a javascript pack tag calling a pack called "application." The div is where React will insert it's JSX. The pack tag is what calls our React code.

    # app/views/pages/index.html.erb 
    <div id="root"></div>
    <%= javascript_pack_tag 'hello_react' %>
    

    When we generated our application with the webpack option, Rails created a directory we haven't seen before: app/javascript. This is where all the Webpack and React code is. This is a whole application within our Rails application. We still have the app/assets/javascripts folder as part of the asset pipeline. That is where you put miscellaneous JavaScript code for our regular Rails views. Inside app/javascript you'll see a folder called packs with two files: application.js and hello_react.js. The second file is what we are calling with <%= javascript_pack_tag 'hello_react' %>. Now go to the browser. If the server is still running then go to localhost:3000 and you should see "Hello React." React is working!

    The hello react file is just to see if it works. We'll use the 'application' pack file instead. Change the index page to:

    # app/views/pages/index.html.erb 
    <div id="root"></div>
    <%= javascript_pack_tag 'application' %>
    

    React Setup

    Now lets switch to the React portion of our code. We went into detail about React in part 2 of this tutorial so we'll only add explanations for anything that is different. First let's add any packages we need. Right now we only need one, react-router-dom. We aren't loading the axios package like we did in Part 2 because we'll be using the ES6 fetch API for our AJAX calls so no additional package is needed. We can still use Axios of course, but it will be interesting to see how we do it with Fetch.

    yarn add react-router-dom

    File Structure

    Let's set up our file structure. You can use these Unix commands (or do it manaually in your text editor).

    rm app/javascript/packs/hello_react.jsx
    mv app/javascript/packs/application.js app/javascript/packs/application.jsx
    mkdir app/javascript/components
    touch app/javascript/components/App.jsx
    touch app/javascript/components/Home.jsx
    touch app/javascript/components/ArticleList.jsx
    touch app/javascript/components/ArticleInfo.jsx
    touch app/javascript/components/ArticleAdd.jsx
    touch app/javascript/components/ArticleEdit.jsx
    

    We removed the hello_react file. We renamed the application.js file to application.jsx since it will be containing JSX. Nothing special about the name "application." We could call it "articles.jsx" if we had multiple packs. JavaScript files in the packs folder are referred to as "packs." They serve as entry points to our front-end code. Think of packs as bridges between your Rails views and your React components. Application.jsx is the equivalent of the src/index.js file generated by create-react-app from the Part 2 tutorial. We are calling it from the app/views/pages/index.html.erb file.

    We need a folder to hold our components. Since our app is small we just put all our components in one folder called components. Note that our components folder is a sibling to, and not inside the packs folder.

    Assets

    We still have our Rails asset pipeline and since we are calling our React components from a traditional Rails view file, it has access to all the classes in the app/assets/stylesheets directory. For convenience we'll leverage the Bootstrap CDN from our Rails stylesheet (after changing the extenson to scss). Feel free to use your own styles if you prefer, or to load the Bootstrap gem. You could also put the CDN link in the app/views/layouts/application.html.erb file head element if you prefer.

  • mv app/assets/stylesheets/application.css app/assets/stylesheets/application.scss
  • # app/assets/stylesheets/application.scss
    @import url('https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css');
    

    If you wanted to put some or all of your stylesheets in with your React code you can do that. You would just make a directory in the packs folder

    mkdir app/javascript/packs/stylesheets

    Then add one or more css or scss files in there. However you will need to call these from the Rails view with a stylesheet pack tag. If your css file was app/javascripts/packs/stylesheets/application.css you could call it from your Rails view file with <%= stylesheet_pack_tag 'stylesheets/application' %>


    React Components

    Let's set up our React front end like we did in the last tutorial. Start with the pack file. We'll change this shortly but let's make sure it's going to work first.

    # app/javascript/packs/application.jsx
    import React from 'react';
    import ReactDOM from 'react-dom';
    import Home from '../components/Home';
    
    ReactDOM.render(<Home />, document.getElementById('root'));
    

    Populate the Home component.

    # app/javascript/components/Home.jsx
    import React from 'react';
    
    const Home = (props) => {
      return (
        <div className="jumbotron">
          <h1>Home Page</h1>
        </div>
      );
    }
    
    export default Home;
    

    We need to restart the server this time. Ctrl+C to stop.

    foreman start -f Procfile.dev -p 3000

    Now go to http://localhost:3000 and you should see Home Page. Now that we know it's working, change the pack file to point to App.jsx instead of Home.jsx:

    # app/javascript/packs/application.jsx
    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from '../components/App';
    
    ReactDOM.render(<App />, document.getElementById('root'));
    

    # src/components/App.jsx

    Lets set up the App component that contains our Router and an element to insert our various CRUD components when called. This component will be the same as in Part 2 of this tutorial, except we will use React Router's HashRouter instead of the BrowserRouter. HashRouter will insert a hash sign # in our routes to distinguish React Routes from Rails Routes. Without it, since our front end and API are now in the same domain, Rails would be trying to match the URL to the routes set in the config/routes.rb file. It won't find them. That is a downside of integrating React with the Rails app. If you don't like the hash signs in your URLs, it is out of scope for this tutorial, but there are other workarounds that require some configuration changes.

    # app/javascript/components/App.jsx 
    import React, { Component } from 'react';
    import Home from './Home';
    import ArticleList from './ArticleList';
    import ArticleAdd from './ArticleAdd';
    import ArticleInfo from './ArticleInfo';
    import ArticleEdit from './ArticleEdit';
    import {HashRouter as Router, Route, NavLink, Switch} from 'react-router-dom'
    
    class App extends Component {
      render() {
        return (
          <div className="App">
            <Router>
              <div className="container">
                <Navigation />
                <Main />
              </div>
            </Router>
          </div>
        );
      }
    }
    
    const Navigation = () => (
      <nav className="navbar navbar-expand-lg navbar-dark bg-dark">
        <ul className="navbar-nav mr-auto">
          <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/">Home</NavLink></li>
          <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/articles">Articles</NavLink></li>
        </ul>
      </nav>
    );
    
    const Main = () => (
      <Switch>
        <Route exact path="/" component={Home} />
        <Route exact path="/articles" component={ArticleList} />
        <Route exact path="/articles/new" component={ArticleAdd} />
        <Route exact path="/articles/:id" component={ArticleInfo} />
        <Route exact path="/articles/:id/edit" component={ArticleEdit} />
      </Switch>
    );
    
    export default App;
    

    Save the above code to the App.jsx file, and now in your browser and you should see the Home Page along with a navigation bar.


    # src/components/ArticleList.jsx

    Now let's see if our API connection is working. Populate the ArticleList file.

    import React, { Component } from 'react';
    import { Link } from 'react-router-dom';
    
    class ArticleList extends Component {
      constructor() {
        super();
        this.state = { articles: [] };
      }
    
      componentDidMount() {
        fetch('api/articles')
          .then(response => response.json())
          .then(data => {
            this.setState({articles: data});
          })
          .catch(error => console.log('error', error));
      }
      render() {
        return (
          <div>
            {this.state.articles.map((article) => {
              return(
                <div key={article.id}>
                  <h2><Link to={`/articles/${article.id}`}>{article.title}</Link></h2>
                  {article.content}
                  <hr/>
                </div>
              )
            })}
            <Link to="/articles/new" className="btn btn-outline-primary">Create Article</Link> 
          </div>
        );
      }
    }
    
    export default ArticleList;
    

    There are two differences from the previous version. First is the URL. The call to the API is not the full URL, it's just the path. That's due to the Webpacker integration. We also now have the "api" namespace. And we don't need to append .json to the URL since the controller actions are now rendering only JSON.

    Second is we are using the ES6 fetch API to make our AJAX type calls to the server. So we don't have to import the Axios library or any library since fetch is build into JavaScript now. Let's compare the componentDidMount() method using Axios vs using Fetch.

    // Using Axios
    componentDidMount() {
      axios.get('api/articles')
        .then(response => { 
          this.setState({articles: response.data});
        })
        .catch(error => console.log('error', error));
    } 
    
    // Using Fetch
    componentDidMount() {
      fetch('api/articles')
        .then(response => response.json())
        .then(data => {
          this.setState({articles: data});
        })
        .catch(error => console.log('error', error));
    }
    

    The only difference is with fetch there is an extra step: .then(response => response.json()). Here ES6 promises waits for the HTTP response back from the server after your fetch request. Then the json() method collects the "body" stream from the response and parses it from JSON text into JavaScript, and returns the result to the next step.

    Axios does this step for you. For one extra line of code with fetch you don't need another library, and in this basic tutorial both Axios and Fetch perform the same. However, as your application gets more complex you may find Axios has some capabilities that fetch does not, so which you use depends on what you need. Good to know both ways regardless.


    # src/components/ArticleInfo.jsx

    Populate the ArticleInfo file. Again the same as the previous tutorial except for the API URL and the use of Fetch. Save it and confirm it's working in the browser.

    import React, { Component } from 'react';
    import { Link } from 'react-router-dom';
    
    class ArticleInfo extends Component {
      constructor() {
        super();
        this.state = { article: {} };
        this.handleDelete = this.handleDelete.bind(this);
      }
    
      componentDidMount() {
        fetch(`api/articles/${this.props.match.params.id}`)
          .then(response => response.json())  
          .then(data => {
              this.setState({article: data});
          })
          .catch(error => console.log('error', error));
      }
    
      handleDelete() {
        fetch(`api/articles/${this.props.match.params.id}`, {method: 'DELETE'})
          .then(() => {
            this.props.history.push("/articles")
          })
          .catch(error => console.log('error', error));
      }
    
      render() {
        return (
          <div>
            <h2>{this.state.article.id}: {this.state.article.title}</h2>
            <p>{this.state.article.content}</p>
            <p>
              <Link to={`/articles/${this.state.article.id}/edit`} className="btn btn-outline-dark">Edit</Link> 
              <button onClick={this.handleDelete} className="btn btn-outline-dark">Delete</button> 
              <Link to="/articles" className="btn btn-outline-dark">Close</Link>
            </p>
            <hr/>
          </div>
        )
      }
    }
    
    export default ArticleInfo;
    

    # src/components/ArticleAdd.jsx

    Populate the ArticleAdd component file. POST and PATCH/PUT requests are sending data to the API so the fetch call needs to include the header property telling the API that you are sending data in JSON format. The body property contains that data you are sending. You need to convert the from data from a JavaScript object to a JSON string using the JSON.stringify() method.

    import React, { Component } from 'react';
    
    class ArticleAdd extends Component {
      constructor() {
        super();
        this.state = { title: '', content: ''};
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
        this.handleCancel = this.handleCancel.bind(this);
      }
    
      handleSubmit(event) {
        event.preventDefault();
        fetch('api/articles', {
            method: 'POST',
            body: JSON.stringify(this.state),     
            headers: {'Content-Type': 'application/json' }
          })
          .then(response => response.json())
          .then(data => {
            this.props.history.push(`/articles/${data.id}`);
          })
          .catch(error => console.log('error', error));
      }
    
      handleChange(event) {
        this.setState({ [event.target.name]: event.target.value });
      }
    
      handleCancel() {
        this.props.history.push("/articles");
      }
    
      render() {
        return (
          <div>
            <h1>Create Article Post</h1>
            <form onSubmit={this.handleSubmit}>
              <div className="form-group">
                <label>Title</label>
                <input type="text" name="title" value={this.state.title} onChange={this.handleChange} className="form-control" />
              </div>
              <div className="form-group">
                <label>Content</label>
                <textarea name="content" rows="5" value={this.state.content} onChange={this.handleChange} className="form-control" />
              </div>
              <div className="btn-group">
                <button type="submit" className="btn btn-dark">Create</button>
                <button type="button" onClick={this.handleCancel} className="btn btn-secondary">Cancel</button>
              </div>
            </form>
          </div>
        );
      }
    }
    
    export default ArticleAdd;
    

    Create and delete a few articles to make sure it's working. Then on to the last component.


    # src/components/ArticleEdit.jsx

    Like with Axios you can use either the PATCH or PUT HTTP method to submit updates. Add the below code to the ArticleEdit component.

    import React, { Component } from 'react';
    
    class ArticleEdit extends Component {
      constructor() {
        super();
        this.state = { title: '', content: ''};
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
        this.handleCancel = this.handleCancel.bind(this);
      }
    
      componentDidMount() {
        fetch(`api/articles/${this.props.match.params.id}`)
          .then(response => response.json())
          .then((data) => {
            this.setState(data);
          })
          .catch(error => console.log('error', error));
      }
    
      handleSubmit(event) {
        event.preventDefault();
        fetch(`api/articles/${this.props.match.params.id}`, {
            method: 'PATCH',
            body: JSON.stringify(this.state),   
            headers: { 'Content-Type': 'application/json' }
          })
          .then(response => response.json())
          .then(data => {
            this.props.history.push(`/articles/${this.state.id}`);
          })
          .catch(error => console.log('error', error));
      }
    
      handleChange(event) {
        this.setState({ [event.target.name]: event.target.value });
      }
    
      handleCancel() {
        this.props.history.push(`/articles/${this.state.id}`);
      }
    
      render() {
        return (
          <div>
            <h1>Edit {this.state.title}</h1>
            <form onSubmit={this.handleSubmit}>
              <div className="form-group">
                <label>Title</label>
                <input type="text" name="title" value={this.state.title} onChange={this.handleChange} className="form-control" />
              </div>
              <div className="form-group">
                <label>Content</label>
                <textarea name="content" rows="5" value={this.state.content} onChange={this.handleChange} className="form-control" />
              </div>
              <div className="btn-group">
                <button type="submit" className="btn btn-dark">Update</button>
                <button type="button" onClick={this.handleCancel} className="btn btn-secondary">Cancel</button>
              </div>
            </form>
          </div>
        );
      }
    }
    
    export default ArticleEdit;
    

    Now test it out. You should have a fully functioning CRUD app with a Ruby on Rails back end and React front end. This time in an integrated application running in the same domain. Cheers.