Ruby

Using React Inside Your Rails Apps

I have never felt as productive as I do in Rails. Yet, with front-end seemingly moving further away from server-rendered views toward React, Angular, Vue, and Ember, I was unsure where Rails fit into this picture. Would it be relegated only to apps with “simple” front-ends, where holding things together with jQuery still managed to work, or perhaps to its new API mode, serving up data via RESTful JSON feeds to be consumed by client-side code and mobile apps?

The answer to that question may be yes, because Rails still does an amazing job at that. But perhaps the answer lies more in the realm of “maybe…it depends”. Would it be better to bring the best of the front-end and have it live happily within the Rails ecosystem?

In this article, Marian Serna and I will explore different ways of using React inside of your new or existing Rails apps.

Rails Embraces JavaScript

JavaScript has its flaws and that’s why Rails embraced CoffeeScript early on instead of plain JS. With ES6 and Babel, CoffeeScript seemed less and less necessary, and in fact to me became something I disliked writing when most examples and articles online were using ES6 syntax. The js2coffee website came in handy to convert between the two syntaxes.

Then came the issue of how to include JS libraries in your Rails app. The typical way would be to look for a “Rails” version of the JS library on rubygems.org, or perhaps take advantage of rails-assets.org. It was a good solution for the time, but to fully embrace JS and provide a more seamless integration, we needed the ability to have a package.json file to list our JS dependencies, install them via yarn, and then package them together with a tool like webpack.

With Rails 5.1, it feels like we finally have a great JS story rather than one that is just passable. It comes with support for yarn, es6, and webpack. Read here for a great intro to exactly what this includes. Also, though it’s a paid course, I recommend the ES6 course by Wes Bos as a great introduction to the new features of JS.

Rails Embraces React

React on Rails is a gem that allows you to organize and use React inside of a Rails project, easily rendering components (and passing them data) from within your Rails views. Additionally, it allows you to take advantage of NPM JavaScript libraries with yarn.

Although Rails 5.1 gives you some of this ability, React on Rails presents a nice alternative for people who want the best of the front-end within Rails. It creates a structured and complete solution where any front-end developer, who may not have any prior Rails experience, can jump in and feel at home.

Installation

For further details on installing React on Rails, please refer to the installation guide.

The basic steps are as follows:

  • gem react_on_rails, ~> 7.0, >= 7.0.4 + bundle install
  • Commit
  • Run rails generate react_on_rails:install --help
  • We want to install it with Redux: rails generate
    react_on_rails:install --redux --node

This will prepare your Rails application to be able to render React components by creating a client folder where all of your React work will take place. Additionally, it adds a ‘HelloWorld’ example that includes a controller, route, view, and a component. Use git diff to see which files have been added and/or modified.

One caveat we’ve noticed with Rails 5.1 is that since it comes with a package.json file of its own, there may be a conflict and you will have to manually merge them together.

Inside of the client folder, you will find a fully separate React setup that comes with its own package.json file, containing all of the NPM modules necessary to run React with Webpack and ES6. Webpack comes preconfigured. All of our components will live inside of client/app/bundles.

Our First Component

You can use as much or as little of React as you want in your Rails app. It doesn’t need to take over the entire front-end. Here is an example using React to create a hamburger nav menu; one small piece of a larger page.

Inside of the application layout file, a React component is rendered using the react_component helper method provided by react_on_rails.

!!! 5
%html
  %head
    ... typical head tags

  %body
    = react_component("NavMenu", props: {}, prerender: false)

This produces the following HTML, which is then picked up by react_on_rails, and a component is initialized on your behalf.

<script type="application/json" class="js-react-on-rails-component">{"component_name":"NavMenu","props":{},"trace":false,"dom_id":"NavMenu-react-component-f8e24b6a-a711-49e5-930b-1472e97665ec"}</script>
<div id="NavMenu-react-component-f8e24b6a-a711-49e5-930b-1472e97665ec"></div>

The actual component renders a material design hamburger menu using the react-mfb package. This component lives in client/app/bundles/App/components/NavMenu.jsx.

import React from 'react';
import {Menu, MainButton, ChildButton} from 'react-mfb';

export default class NavMenu extends React.Component {
  render() {
    return(
      <Menu effect="zoomin" method="click" position="tr">
        <MainButton iconResting="ion-drag" iconActive="ion-close-round" />

        <ChildButton icon="ion-ios-home" label="Home"
          onClick = {(e) => {}} href="/" />
        <ChildButton icon="ion-android-laptop" label="Work"
          onClick = {(e) => {}} href="/work" />
        <ChildButton icon="ion-android-person" label="About"
          onClick = {(e) => {}} href="/about" />
        <ChildButton icon="ion-email" label="Contact"
          onClick = {(e) => {}} href="/contact" />
      </Menu>
    )
  }
}

One important note is how to expose your React components to Rails. Inside of the client/app/bundles/App/startup/registration.jsx file, you can register it:

import ReactOnRails from 'react-on-rails';
import NavMenu from '../components/NavMenu';

ReactOnRails.register({
  NavMenu
});

Visiting any page on www.marianserna.com, you’ll see the hamburger menu on the top-right hand corner of the screen.

Passing Data from Rails to React

In React, components generally have two types of “data” to worry about: Props and State. Props are read-only data passed from a parent component to a child. State is data internal to and managed by a specific component. If you need state, that is up to you, but how you pass props from your Rails view to your React component is done like so:

= react_component("CaseStudy", props: {case_study: @case_study_props, code_highlights: @code_highlights_props, more_case_studies: @more_case_studies_props}, prerender: false)

These props will then arrive to the component’s constructor function as a JS object, which you can then access with this.props.case_study, for example.

export default class CaseStudy extends React.Component {
  constructor(props, _railsContext) {
    super(props);

    // ... rest of constructor
  }
}

To see the rest of the component, it is available here and you can see it in action here.

Posting Data Back to Rails

When you need to send data back to Rails, you have quite a few different options. You could use jQuery.post, fetch (remember to include a polyfill for older browser support), or a library like axios. These are just standard AJAX requests like you’re used to. In this case, we’re treating our Rails app like an API, usually expecting JSON as the response.

In a slightly more in-depth react_on_rails integration, this is how you might post data to create a House object that has many related Image records. This takes advantage of Rails’ accepts_nested_attributes_for ability to create parent and child records at the same time. The code is available on GitHub. Refer to the repo and the controllers/models/serializers to see how the Rails side of this example was done.

submitForm = (e) => {
  e.preventDefault();

  // Build form data and format it in way Rails API expects
  const data = new FormData();
  data.append('house[city]', this.city.value);
  data.append('house[price]', this.price.value);
  data.append('house[description]', this.description.value);
  Array.from(this.images.files).forEach((file, index) => {
    data.append(`house[images_attributes][${index}][photo]`, file, file.name);
  });

  // Include CSRF token for form_authenticity validation
  const config = {
    headers: {
      'Content-Type': 'multipart/form-data',
      'Accept': 'application/json',
      'X-CSRF-Token': ReactOnRails.authenticityToken()
    }
  };

  // Post data to /houses endpoint and update state upon response
  axios.post('/houses', data, config).then((response) => {
    this.setState({
      status: 'success',
      houseId: response.data.id
    });
  });
}

One thing you might have noticed is the ability to include the CSRF token in our POST request to Rails. This is functionality provided by react_on_rails and must be imported at the top of your file: import ReactOnRails from 'react-on-rails';. The complete file can be found here.

!Sign up for a free Codeship Account

Managing State in Redux

As your app grows in complexity, it may be a good idea to avoid managing state in each of your components (or a single Main/Parent component) and instead manage it using Redux. The talented Wes Bos again has a course for learning this, which happens to be free!

react_on_rails comes with built-in support for Redux and all of the packages are included if you install it with the --redux option.

Redux is very verbose and in my opinion sort of complicated to get set up. Thankfully the HelloWorld example of react_on_rails contains a complete setup. What you’ll see are folders for the following items:

  • store: This is where you will define your Redux data store (or global state). You can include any middleware you may want, like thunk, which we’ll touch on very briefly below.
  • actions: Actions in redux are functions which are in charge of preparing data and talking with external resources before finally dispatching an event with the goal of updating the store’s state. Your business logic lives here. These work hand in hand with reducers which we’ll talk about below.
  • constants: These are constants so that the actions and reducers can refer to the same events. It is how the action tells the reducer which data is going to be changed.
  • reducers: Reducers have the task of taking the existing copy of the store’s state and producing a new version of it based on the data dispatched/returned from an action.
  • containers: This is the glue between a Component and the Redux store + actions. They are small wrappers which pass the Redux store as props to the component. Any time a store’s data is updated, the component will receive those updates via props to force a rerender.

Without going into the full complexity of Redux, we’ll try to show a simple example below that fetches the information for a single House object in our demo app.

When the HouseInfo component mounts, it will make a call to the action, telling it which house it needs data for.

componentWillMount() {
  this.props.loadHouse(this.props.match.params.id);
}

The loadHouse action lives inside of a file which contains all related actions. Because this AJAX request happens asynchronously, our loadHouse will return a function that will get passed a dispatch function. dispatch can be called any time we have more information we want to update in the store (via a reducer).

For more information on dealing with asynchronous code inside of Redux, check out the redux-thunk library.

export const loadHouse = (id) => {
  return (dispatch) => {
    // Make sure there isn't an existing house in the store
    // Also allows us to show a "Loading..." screen
    dispatch({type: LOAD_HOUSE, house: null});

    // Make AJAX request to Rails endpoint
    axios.get(`/houses/${id}`, {
      headers: {'Content-Type': 'application/json', 'Accept': 'application/json'}
    }).then((response) => {
      // Upon response, dispatch another event with the house data
      dispatch({
        type: LOAD_HOUSE,
        house: response.data
      });
    });
  }
}

Now it’s time for the reducer to create a new version of the state based on the events dispatched above.

// Load our constants, the link between actions & reducers
import {
  LOAD_HOUSE,
  LOAD_FEATURED_HOUSES,
  SEARCH_HOUSES
} from '../constants/homeConstants';

const homeReducer = (state = {}, action) => {
  // Look for the action.type to determine what state needs to change
  switch (action.type) {
    case LOAD_HOUSE:
      // Create a copy and then modify copy of state using ES6 spread operator
      return {...state, house: action.house};
    case LOAD_FEATURED_HOUSES:
      return {...state, houses: action.houses};
    case SEARCH_HOUSES:
      return {...state, houses: action.houses};
    default:
      // We didn't understand any of the action types, return existing state unchanged
      return state;
  }
};

export default homeReducer;

Client Side Routing in React

If we want routing to be handled client-side using React Router, this isn’t a problem with react_on_rails. The trick to make it work is that you’ll essentially have to handle routing twice…once inside of Rails (which will render the component) and then again inside of React.

The Rails routes are pretty standard:

Rails.application.routes.draw do
  root to: "houses#index"

  resources :houses, only: [:new, :show, :create] do
    collection do
      get :featured
      get :search
    end
  end
end

What you don’t see here is that houses#index, houses#show, and houses#new all render the same component:

= react_component("HomeApp", props: {}, prerender: false)

This HomeApp component then takes over and figures out the routing on the client side.

const HomeApp = (props, _railsContext) => {
  const store = configureStore(props);

  return (
    <Provider store={store}>
      <BrowserRouter>
        <Switch>
          <Route path="/" exact component={HomeContainer} />
          <Route path="/houses/new" component={NewHouseContainer} />
          <Route path="/houses/:id" component={HouseInfoContainer} />
        </Switch>
      </BrowserRouter>
    </Provider>
  );
};

One thing that caused some confusion was not having Switch wrapping the routes…it was causing ambiguous routing, thinking that the “new” part of "/houses/new" was actually an ID of a house. Switch deals with this by sticking with the route that matches correctly first, so the order you list them in is important.

A Word on Authentication

Because authentication in this demo app was done via Devise, we stuck with using the typical cookie-based user session.

As you move more and more toward your entire app living in React, or even supporting ReactNative or other native apps, you might be better off not assuming that cookies will exist and instead looking into JSON Web Tokens. This is beyond the scope of the article though, so will have to be explored at a later date.

Conclusion

We hope we were able to show that including React inside of your Rails app is not only possible, it is a fairly elegant solution to including more front-end interactivity and avoiding the spaghetti code that jQuery can often end up turning into.

It’s up to you how much or how little to include. It can be as small as a nav component on a specific page, it can be an entire page of your app, or you can use Redux + React Router to take over larger and larger areas of your application.

Don’t feel the need to abandon server-rendered code. It can still be a joy to write and is often the most simple solution. At the same time, it’s great being able to take advantage of all the cutting edge client-side code available these days.

Reference: Using React Inside Your Rails Apps from our WCG partner Leigh Halliday at the Codeship Blog blog.

Leigh Halliday

Leigh is a developer at theScore. He writes about Ruby, Rails, and software development on his personal site.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button