Server Rendering React on Rails

Written by: Leigh Halliday

React apps give us incredible power in the browser, and with the react_on_rails gem from the folks at ShakaCode, we now have an easy way to use React inside of our new and existing Rails apps. In a previous article, I talked about how to get up and running with React in your Rails app.

In this article, we are going to talk about doing server rendering with our React components inside of Rails. An article by Tom Dale talks about misconceptions about server rendering. With so many misconceptions, what is it and why would you want it?

The app we'll be discussing is located here.

What Is Server Rendering?

Usually with React apps, the HTML source returned from the server is extremely minimal, and then it is React itself that builds out the DOM that you end up seeing in the browser. This is in contrast to a traditional server-rendered website where the server generates the HTML, and when it arrives to the browser, it's already fully fleshed out.

What server rendering in React means is to allow the server to prerender the React components server-side before sending them to the browser. When the HTML arrives to the browser, it is displayed immediately as if it were server-rendered, and then React takes over from there. This can provide a few benefits, but as we'll also see, it doesn't come for free.

Benefits to this approach include the potential for better SEO. Crawlers don't need to have JavaScript enabled to see the contents of the page. If you perform a curl call from your terminal, you'll actually see real HTML as opposed to a <div> tag waiting to be populated with content.

Another benefit is that if your components take time to "boot up" or initialize, it can provide a better experience in that they'll be able to see the content of the page more immediately.

Simple Server Rendering

To get up and running with server rendering with the react_on_rails gem, it's as easy as providing an additional parameter to the react_component call and ensuring that you have installed react_on_rails using the --node option. react_on_rails uses a library called MiniRacer to execute JS from within Ruby.

<%= react_component("MyComponent", props: {}, prerender: true) %>

By changing prerender to true, our server produces the following HTML:

<script type="application/json" class="js-react-on-rails-component" data-component-name="MyComponent" data-trace="true" data-dom-id="MyComponent-react-component-a72f0441-f121-4ba5-abc8-6b30dfc60273">{}</script>
<div id="MyComponent-react-component-a72f0441-f121-4ba5-abc8-6b30dfc60273">
  <h1 data-reactroot="" data-reactid="1" data-react-checksum="-22604146">
    Welcome to My Component
  </h1>
</div>

It contains the necessary script tag and ids to allow React (via react_on_rails) to take over in the client, but you can see that the h1 tag arrived pregenerated.

The actual component in this case looks no different from your typical React component:

import React from 'react';
export default class MyComponent extends React.Component {
  render() {
    return (
      <h1>Welcome to My Component</h1>
    );
  }
}

This may work for simple setups, but when you are dealing with ReactRouter and Redux, you'll probably want to opt for a more complicated setup which we'll discuss below.

More Setup Needed

The first issue I ran into when trying to convert a non-server-rendered React app to be server rendered was that I was using BrowserRouter to handle the routing. This is fine in the browser but caused all sorts of issues being executed on the server. I was going to need a slightly different setup for doing server rendering. I would do this through having two separate webpack configs, Procfile entries, and "registration" entry points.

My Procfile.dev file in the root folder of my Rails app had entries for 'web' and 'client'. I added a third for 'server'.

web: rails s -p 3000
client: sh -c 'rm app/assets/webpack/* || true &amp;&amp; cd client &amp;&amp; bundle exec rake react_on_rails:locale &amp;&amp; yarn run build:development:client'
server: cd client &amp;&amp; yarn run build:development:server

Inside of the client/package.json file, I renamed my "script" entries to clearly mention either client or server.

"scripts": {
  "build:production:client": "NODE_ENV=production webpack --config webpack.client.config.js",
  "build:production:server": "NODE_ENV=production webpack --config webpack.server.config.js",
  "build:development:client": "webpack -w --config webpack.client.config.js",
  "build:development:server": "webpack -w --config webpack.server.config.js"
}

For webpack, I took a copy of the client version and modified them both slightly to point at different registration files (one for client and one for server). This is found under the entry key and the full files can be found here.

Router and Server Rendering

In the client version of React, I am using BrowserRouter to handle routing. This won't work on the server because the JavaScript is executing out of the context of a browser, without access to the window or the document objects.

For the server, we can use StaticRouter to kick things off, and then the BrowserRouter will take over once on the client.

The client version

For the client version, the registration file looks like this:

// client/app/bundles/Home/startup/clientRegistration.jsx
import ReactOnRails from 'react-on-rails';
import HomeApp from './ClientHomeApp';
ReactOnRails.register({
  HomeApp
});

And the ClientHomeApp sets up routing and the Redux store.

// client/app/bundles/Home/startup/ClientHomeApp.jsx
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import configureStore from '../store/homeStore';
import routes from '../routes/routes';
export default (props, _railsContext) => {
  const store = configureStore(props);
  return (
    <Provider store={store}>
      <BrowserRouter>
        {routes}
      </BrowserRouter>
    </Provider>
  );
};

The server version

The server version of our registration file looks very similar, only pointing to a different ServerHomeApp file.

// client/app/bundles/Home/startup/serverRegistration.jsx
import ReactOnRails from 'react-on-rails';
import HomeApp from './ServerHomeApp';
ReactOnRails.register({
  HomeApp
});

The ServerHomeApp is where things differ slightly. Here we will swap out the BrowserRouter for a StaticRouter and use the handy railsContext variable to provide the necessary routing context to ReactRouter so it can determine which route to display. This is normally grabbed from the browser directly, but without a browser it's up to us to provide that missing information.

import { Provider } from 'react-redux';
import { StaticRouter } from 'react-router';
import configureStore from '../store/homeStore';
import routes from '../routes/routes';
export default (props, railsContext) => {
  const store = configureStore(props);
  const { location } = railsContext;
  const context = {};
  return (
    <Provider store={store}>
      <StaticRouter location={location} context={context}>
        {routes}
      </StaticRouter>
    </Provider>
  );
};

`

If you are interesting in what the {routes} imported into the router look like, they are as follows (and are thankfully shared across both versions):

// client/app/bundles/Home/routes/routes.jsx
import { Route, Switch } from 'react-router';
import HomeContainer from '../containers/HomeContainer';
import NewHouseContainer from '../containers/NewHouseContainer';
import HouseInfoContainer from '../containers/HouseInfoContainer';
export default (
  <Switch>
    <Route path="/" exact component={HomeContainer} />
    <Route path="/houses/new" component={NewHouseContainer} />
    <Route path="/houses/:id" component={HouseInfoContainer} />
  </Switch>
);

For more details on server-side routing with ReactRouter, feel free to check out their article.

!Sign up for a free Codeship Account

Hydrating our Redux Store

The next part of the puzzle is to hydrate the initial Redux state when doing server rendering. This is done fairly easily thanks to the react_on_rails gem.

We already have the ability to pass props when rendering React components in our Rails views. In a simple case, this is passed directly to our component, but with React we can use it to set the Redux store's initial state.

It starts in the controller by providing the view with the data it needs:

house = House.find(params[:id])
@house_hash = HouseSerializer.new(house).serializable_hash

In the view, we pass the @house_hash on to our component (note that we have prerender: true):

react_component('HomeApp', props: {home: {house: @house_hash}}, prerender: true)

In both the ClientHomeApp and ServerHomeApp files, they receive these props and call a configureStore function.

export default (props, railsContext) => {
  const store = configureStore(props);
  // ...
}

Finally, we use the createStore function from Redux, passing in our props as initial state.

// client/app/bundles/Home/store/homeStore.jsx
import { createStore, combineReducers, applyMiddleware } from 'redux';
import { routerReducer } from 'react-router-redux';
import thunk from 'redux-thunk'
import homeReducer from '../reducers/homeReducer';
const configureStore = (railsProps) => (
  createStore(
    combineReducers({
      home: homeReducer,
      routing: routerReducer
    }),
    railsProps, // our initial state
    applyMiddleware(thunk)
  )
);
export default configureStore;

Now that the store has its initial state set, as Redux passes this state as props to our "connected" component, we can check if these props contain the house we need and thereby avoid having to fetch it via an AJAX call to the server.

// Taken from client/app/bundles/Home/components/HouseInfo.jsx
componentWillMount() {
  // Only load the house from server if it isn't already passed in as a prop
  if (this.needHouse(this.props)) {
    this.props.loadHouse(this.props.match.params.id);
  }
}

Conclusion

In this article, we discussed what server rendering for a React (on Rails) app is and what some of the benefits are. We also saw that it increases the complexity by introducing additional webpack configuration and routing variations on the client versus the server.

In my opinion, I would avoid the extra headache unless you see that it provides real benefits to either your users or to your SEO rankings.

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.