React on Rails
Reverb’s main application is a well loved Rails monolith. It serves our API and view level traffic admirably. As we’ve grown, we’ve added more interactive UI elements, and as we did we followed the Rails Way™. We decorated our behavior with jQuery and we left templating to Rails. This approach, yes even in 2016, is still a great choice for a small amount of UI interactivity, but after a couple of years we began to search for something that could help structure our JavaScript components which were growing in sophistication.
During our search we had a few criteria:
- Plays well with a largely server side rendered application
- Can be mounted in isolation
- Allows for simple unit testing
- Could be easily taught to developers of all levels
React was and is a great fit for our current application. We needed isolated components that could encapsulate complex view logic that could be injected anywhere on the page. We’ve had great success with Ember in the past for standalone applications, but React’s composability provided a better model for allowing us to grow into JS components piece by piece.
Rails has a JavaScript problem
We originally used react-rails to mount our components directly inside Rails views. This provided a quick way to get started, but even after a few components we were seeing similar issues to our jQuery components: dependencies were implicit and exposed via global state. Without adequate JavaScript module support or incremental rebuilds, our React components didn’t solve all of the issues we wanted to address, but got us closer to our goal.
React itself was not the issue, nor really was the react-rails gem to blame (they’re doing the best they can atop the existing asset pipeline). The Rails asset pipeline is showing its age and has struggled to keep up with tools available in the JavaScript ecosystem. We attempted to cobble together a few solutions that decorated the asset pipeline. browserify-rails helped tremendously, but in the end we ditched the asset pipeline in favor of Webpack. JavaScript is simply not a first class consideration in the Rails ecosystem and solutions that ride atop the existing asset pipeline offered more headaches than solutions.
The Switch to Webpack
It took us a few iterations, but eventually we landed on Webpack as our standalone asset build tool. Webpack provided all of the tooling we needed and along with plugins like webpack-manifest we were able to easily compliment the asset pipeline conventions established by Rails.
We ditched the react-rails gem, but adopted its best parts; namely, its view helper and its ujs style javascript library for client side rendering. One downside to this approach is that isolated React components still had to be exposed to window, but any components used within a component could be imported without polluting the global namespace.
Replacing the asset pipeline itself for our JavaScript (and incidentally our stylesheets) was a challenge. We started by isolating all of our React and ES6 JavaScript files into its own folder; a clear dividing line between the Rails asset pipeline and a convenient entry point for our webpack build.
How we React
Using our helper we generate a bit of DOM that is later hydrated by our client side rendering engine. Using this approach we can still inject properties from the server side rendered view, which allows us to build dynamic components that don’t yet need an API to receive the data they need. This provides a fantastic bridge for us to sprinkle in bits of React where needed.
I’ve included here an example of a Rails view helper, a client side renderer, and the breadcrumbs we leave server side to bridge the server to client side gap.
Our view helper looks a whole lot like the one from react-rails. No reason to re-invent something that works. We use this to build a blank div with some data properties that the client side renderer will use to replace with actual React components.
We use this helper alongside our normal Rails rendered views, which allows us to mix in React components as we’re adding UI interactivity. It also allows us to iterate into larger React components without having to replace the entire server side rendered view in a big bang release.
The helper renders a small snippet into the DOM, which becomes a placeholder for our React client side renderer.
After our view is rendered server side, our client side renderer scans the DOM for divs that are waiting to be hydrated by actual React components. These components need to be accessible via window, but we take care to namespace these entry components to prevent collisions. More complex components will usually just use the react-router as its entry point where components are rendered based on the current route.
With these tools in place we had an adaptable platform that allowed us to iterate into client side rendered views where appropriate, replace hard to test jQuery interactions, and as a side effect gave us a great platform to do development in. I can’t express enough, especially in a large Rails application, how empowering tools like Hot Module Replacement, a real module system, spectacular linting tools, and robust testing frameworks are to our ability to iterate quickly on interactions and designs with confidence.
What was that?! Flicker?! I miss Rails views :’(
React’s view abstractions were a considerable improvement over jQuery and its simple API meant quick adoption among the developers here at Reverb. Soon we went from a few isolated components to entire views completely composed of React components. This is when we hit some issues.
On those more complex views where the header and footer were server side rendered, but the body was predominately React rendered, we had to wait and wait and wait as React took control of the DOM and rendered in our components. This can cause some jarring UI experiences.
Even where we carefully planned for good empty states we were left with UI components that flickered into existence and caused the DOM to shift around as views were hydrated. This can be mitigated by guessing at div sizes and creating good empty containers and we even went so far as to server side render our empty states with Rails only to have React remove those empty divs as they mounted, but this was far from ideal. It meant maintaining identical DOM partials in both React components and Rails’ views and clever use of jQuery to remove those empty divs once the React component was in place.
We heard the siren song of universal javascript, but felt late to the party. Our backend didn’t speak JavaScript. There have been some attempts at solving this problem in Rails and we learned a lot from reading over @shakacode’s react_on_rails examples, but found that some of their server side JavaScript tooling did not quite meet our needs. Their approach seemed sane though and we started to iterate on a solution to better meet our needs.
How I Learned to Stop Worrying and Love Node.js
Now that our JS was being shipped to our frontend in bundles produced by Webpack, we figured we could deliver that same bundle to a Node.js application to help out our Rails backend in rendering some server side React. React comes with server side rendering support out of the box, but we had to do some extra work to get our routed components to work well both client and server side.
At Reverb, we’ve been using react-router for client side routing and we didn’t want to have to maintain yet another routing stack server side. Luckily after a bit of finagling, cursing at docs, and ample finger crossing we were able to put something together that worked perfectly on both sides of the stack.
While we ship the same Routes.jsx file to the server and client, the server side code uses a slightly different (non browser history based) router to accomplish the same task as our client side Router.
The React Rendering Engine
Returning Results from Rails
While this simple Node.js app could render our React components we still needed a way to get these back into our Rails views. With a few modifications to our `react_component` helper we were able to call through to Node.js when needed.
Since we deploy our React Rendering Engine alongside our core Rails stack, we utilize a socket connection to communicate between the two frameworks. This separation of concerns allows our React Rendering Engine to be shared between a variety of different stacks and languages as we grow.
With these pieces in place we can serve React snippets from our backend that are later picked up by our client side renderer. This means a dramatic difference in the UX for those views that were predominately rendered via React without forcing us to use arbitrary blank containers divs. In fact we can deliver many side effect free components (ie. those that contain no external API calls) instantly to the user.
Flicker be gone!
React has been a great boon to view development here at Reverb. With React components we can easily unit test our views, close the iteration gap with Hot Module Reloading, and provide isolated re-usable components across our application platforms. Work like this gets us closer to creating the kind of experience we want for our users.
Have some questions? Feel free to reach out. We’ll be evaluating extracting some of these components into open source projects in the coming weeks so keep your ear to the ground, you might like what you hear.
This work was the result of some great efforts from: