Server Rendering React on Rails
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 && cd client && bundle exec rake react_on_rails:locale && yarn run build:development:client' server: cd client && 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.
Reference: | Server Rendering React on Rails from our WCG partner Leigh Halliday at the Codeship Blog blog. |