Server-rendered React components in Rails

Warning: experimental stuff ahead. Make sure you know more about running Rails in production than I do before you take this & run with it.
Source on GitHub


Here's my favourite slide from Pete Hunt's JSConf EU talk on React (which you should totally watch).

"That is, React components are basically just idempotent functions. They describe your UI at any point in time, just like a server-rendered app."

If you render a React component with the same data, it will always return the same result. It makes absolutely no difference whether you do that rendering on the client or the server.

That brings us to the official front end buzzword of 2014: Isomorphic JavaScript.

The Holy Grail. The united UI layer. Serve up real HTML on first page load, then kick off a client side JS app. All without duplicating a single line of UI code.

React makes this really easy if your back end is Node.js:

// Our component...
var HelloMessage = React.createClass({
  render: function() {
    return <div>Hello {this.props.name}</div>;
  }
});

// ...can be rendered to a string on the server...
React.renderComponentToString(HelloMessage({name: "John"}));

// ...then on the client, renderComponent will preserve
// the server-rendered markup & just attach event handlers
React.renderComponent(<HelloMessage name="John" />, mountNode);

Awesome? Awesome. Except I spend most of my day working on a big old Rails app.

The react-rails gem

The official react-rails gem is pretty great. The current 1.0.0.pre release gives you

  • React
  • Asset pipeline JSX compilation
  • An unobtrusive JS adapter that automatically mounts React components on HTML elements that have special data attributes (similar to jquery-ujs)
  • A view helper react_component for generating that mount node

By default, the react_component helper generates an empty div with data attributes, onto which react_ujs comes along and mounts a component.

However, thanks to John Lynch, if you specify the :prerender => true option the helper actually executes (via ExecJS) React.renderComponentToString and renders the component server side.

The Rails code

I built a tiny, super simple, stock Rails app using the CommentBox component from the React tutorial to try this out. It looks like this:

"app screengrab"

The whole thing is a React component composed of smaller React components, all server-rendered using the exact same JS files that are used by the client side JS app.

Here's index.html.erb in its entirety:

<%= react_component('CommentBox', 
                    {:presenter => @presenter.to_json}, 
                    {:prerender => true}) %>

The Rails controller defines a presenter that gives the component all the initial data it needs to render.

def index
  @presenter = {
    :comments => Comment.last(5),
    :form => {
      :action => comments_path,
      :csrf_param => request_forgery_protection_token,
      :csrf_token => form_authenticity_token
    }
  }
end

When the page loads, react_ujs sees the component & calls React.renderComponent on it, which is smart enough to preserve the server rendered markup & just add event handlers.

With the client side app bootstrapped, the form submits via ajax & adds the new comment to the list. But if the JS fails to load or initialise, the form will submit normally with a full page refresh. Progressive enhancement ahoy!

You need to make the Rails controller handle both the ajax & non-ajax scenarios.

def create
  @comment = Comment.new(comment_params)
  @comment.save

  if request.xhr?
    render :json => Comment.last(5)
  else
    redirect_to comments_path
  end
end

Back in the client side CommentBox component, when the ajax request completes, we simply update it with the new state & React does its thing.

$.ajax({
  // ...
  success: function ( data ) {
    this.setState({ comments: data });
  }.bind( this )
});

This is so awesome!

You don't need a DOM on the server and, React being React, you're not even really thinking about the DOM on the client. Because components are basically just idempotent functions, you can render them in any environment you like as long as you have some data representing the current state and the ability to execute JS.

There are some rough edges. Forms are harder. There are no Rails view helpers in React land. Passing in CSRF tokens & hand coding a hidden field every time is gonna get old fast.

That said, after playing around with this for a day, using different templates & languages depending on whether you're rendering on the client or server feels totally arbitrary & stupid. The sometimes ERB, sometimes Handlebars workflow feels so kludgy now.

I'm looking forward to this future :D

Side note: If you don't want to execute JS right there in your Rails app, another interesting approach is to spin up a Node.js app whose sole responsibility is rendering React components to strings. There's an example of this views-as-a-service-over-http setup in the React repo.