Recently I’ve been digging into the world of React and ES6. I was keen to get these working in a Rails context. The key things I wanted to achieve were server-side rendering, ES6 syntax JavaScript, react-router and to import node modules. On a heavily delayed train journey from London to Edinburgh I decided to try crack it.
There are a bunch of gems that can get us most of the way with very little configuration. However all of these don’t get you quite to my desired feature combination, but with a bit of tweaking it is possible.
There is React-Rails
We can use use the react-rails gem to get moving pretty fast. It adds both JSX and Babel transpilers to the asset pipeline so you can generate your React files just like any other Rails asset with the .jsx suffix. The gem is also maintained (or at least approved) by the React organisation, so it feels like an officially supported way to use React.
Most beneficial of all is it provides a simple view helper that can be used in your views to render your React component to the HTML server side. It also provides a script that will start your react client-side app automatically based on data attributes from the component you rendered server side.
<%= react_component(component, arguments, options) %>
The downside of this automagic is both of these renderings happen before the point you’d hook up the react-router to your app. Luckily someone has already solved that.
React-Router-Rails to the rescue
Another gem very similar to react-rails has been created by mariopeixoto called react-router-rails. It provides very similar functionality to react-rails, however where react-rails renders a component react-router-rails renders a router.
<%= react_router(component, arguments, options) %>
Using this you’re soon creating a react app with routing. It’s very cool.
However what nagged at me was how the asset pipeline affected matters. By using this everything had to be shared via global variables. This didn’t feel very natural in the context of React:
// Not very react
(function(Components) {
Components.App = React.createClass({
render: function () {
return <ReactRouter.RouteHandler />;
}
});
})(Project.Components);
External libraries also needed to be introduced via global variables rather than the frequently seen CommonJS module system.
This left me at the staring point of a simple React-Router powered Isomorphic JS app, but it didn’t feel quite right. It felt like I had one foot in the present, one in the past (albeit the recent past given the pace of client-side developments :-).
Enter browserify-rails
There are two popular projects bringing the world of node modules to the browser, Browserify and Webpack. Webpack is the one that aims to do a bit more, with the aim of bundling all your JS deps with some cool hot-reload functionality, whereas Browserify is a bit simpler only focussing on node modules.
In the realms of Rails, browserify seemed the more attractive bet. People have done cool integrations with Webpack but that struck me as problematic for server-side rendering.
Like most things Rails, someone has already built it and released the gem. There is browserify-rails out there which will run browserify against your JS as part of the asset pipeline. Instead of using sprockets to require files, it can all be done via CommonJS imports.
Browserify can also run babel (via babelify) over the JS files to transpile both the ES6 and JSX syntaxes.
Using the ES6 syntax does cause an initial problem that browserify-rails doesn’t actually realise it is supposed to transpile the file as it looks for presence of CommonJS ES5 syntax. This can be resolved by including a comment of // require, by configuring the path or by setting everything to transpile.
Bringing them all together
Each of these work pretty good together but bringing them all together produced a few hurdles to overcome.
Firstly it felt a bit weird having some client side dependencies served by gems (react & react-router) and others via NPM. So I felt it was easier to switch those out to NPM and not use the bundled gem ones.
This switch revealed that I had both broken the existing code (by not importing React) and had an environment that wasn’t easy to debug (it doesn’t report which JS file has an error server side). In the heat of this I believed (incorrectly) that problems lay in react-router-rails and how it deviated from keeping up with react-rails.
I decided a safer route was to stick to just using the react-rails and include the changes needed for routing myself via changing the renderer via the react-rails config and introducing a new view helper method (mostly copied from react-router-rails). I’m not sure if I’ve made a great decision here or not, on one hand I’ve killed a dependency, the other I’ve added a concern. Ideally it’d just be easier in react-rails to specify the JS that is used to initialise the app server-side.
Is it any good?
It all works and is cool but does have a couple of issues. The browserify stuff feels a bit fragile, particularly on the server-side. I’ve had a few issues where I’ve felt like restarting the server or clearing cache were required to get the most up to date code executed. I’m too early into this project to know if that’ll be a consistent issue.
Whether Rails is the right tool in a stack like this feels contentious. For the project I’m working on, the freebies you get from the eco-system make it feel like the right choice. But for many projects it’d be smarter to use a node/express setup with webpack. Some of the pains should be eased when Sprockets ES6 comes with Rails 5, but at the pace of change within client-side development it might feel like it’s behind in a different way.
Anyway, now to pick a Flux implementation. That can’t be too hard right?