/react-isomorphic-boilerplate

Isomorphic Server & Browser Side Rendering with React >= 0.12

Primary LanguageJavaScript

This is Part 1 of the series "Modular Isomorphic React JS applications". See Part 2 and Part 3 for more.

Isomorphic Server & Browser Side Rendering with React >= 0.12

tl;dr: Using React + Handlebars + Browserify to render your DOM server side while keeping React's awesomeness browser side too.

Introduction

tl;dr: React's virtual DOM is smart and powerful.

React is now famous for utilizing a virtual DOM to implement exceptionally quick and efficient re-renders in the browser.

Thanks to the virtual DOM, we don't actually need a browser or DOM implementation to begin rendering. In fact, React gives us enough to render a DOM tree to raw HTML anywhere we can run Javascript. And that includes node/io.js on the server!

One of React's real strengths is the ability to seamlessly understand React-rendered HTML in both the sever and the browser. For example; render a DOM tree once on the server, then call .render() again on the browser without any state change, and React is smart enough not to re-render the unchanged DOM. This means we can render critical-path HTML server side with no flash of reloaded JS on the browser!

Let's do it

tl;dr Get the completed example

We'll be using these libraries:

Our code structure will look like this:

- browser       # Browser side logic
- common
  - components  # All our react components
  - controllers # Business logic
- lib
  - components  # Our jsx-compiled components
- public
  - js          # Bundled javascript here
- server
  - templates   # Server side Handlebars templates

Server side rendering

Unlike most React rendering tutorials out there, we will be using the React.renderToString method to generate our DOM. But first, let's start with a simple component:

// file: common/components/todo-item.js
var React = require('react');

module.exports = React.createClass({
  displayName: 'TodoItem',

  getInitialState: function() {
    return { done: this.props.done }
  },

  render: function() {
    return (
      <label>
        <input type="checkbox" defaultChecked={this.state.done} />
        {this.props.name}
      </label>
    );
  }
});

Two important points to note here:

  1. We're using CommonJS style require's to include the React dependency. We will keep this consistent throughout our code, and rely on Browserify to convert that into a browser-compatible version during build time. (See Browser Side Rendering below for more)
  2. We use defaultChecked instead of checked property as React would treat checked as a static property that won't change again. The checkbox should be changeable, and so we only want to set the initial default.

Before we can use the component, we need to convert that JSX into JS using React's tooling:

$ ./node_modules/.bin/jsx common/components/ lib/components/

(This command can be executed with npm run jsx in the example repo)

This assumes you have installed the jsx compiler with npm using npm install --save react-tools. The converted component will be saved in lib/components/todo-item.js

Now, let's render that component on the server:

// file: server/index.js
var React = require('react');
var TodoItem = require('../lib/components/todo-item');

// Since we're not using JSX here, we need to wrap the component in a factory
// manually. See https://gist.github.com/sebmarkbage/ae327f2eda03bf165261
var TodoItemFactory = React.createFactory(TodoItem);

var renderedComponent = React.renderToString(
  TodoItemFactory({done: false, name: 'Write Tutorial'})
);

renderedComponent at this point will be the rendered HTML (cleaned up a little to add whitespace):

<label data-reactid=".e8wbttvlkw" data-react-checksum="-1336527625">
  <input type="checkbox" data-reactid=".e8wbttvlkw.0">
  <span data-reactid=".e8wbttvlkw.1">Write Tutorial</span>
</label>

(Note: it is the data-reactid and data-react-checksum identifiers injected into the rendered output which is where React gets its magic from. From now on, should never have to worry about them other than to know they're important)

As React can't handle rendering <!doctype> tags, conditional comments (for targeting IE), etc, we have to offload that layout rendering task to Handlebars. Here's our simple HTML5 template file:

<!-- file: server/templates/layout.handlebars -->
<!doctype html>
<html lang="">
  <body>
    <div id="content">{{{content}}}</div>
  </body>
</html>

(Note: There is no white space between the <div> and the placeholder {{{content}}} on purpose. See the Gotchyas section for more.)

Our goal is to inject the rendered component into the {{{content}}} tag. Back in the server/index.js file, let's do that:

// file: server/index.js

// [...]

var renderedComponent = // [...]

var Handlebars = require('handlebars');
var fs = require('fs');

var fileData = fs.readFileSync(__dirname + '/templates/layout.handlebars').toString();
var layoutTemplate = Handlebars.compile(fileData);

var renderedLayout = layoutTemplate({
  content: renderedComponent
});

Excellent, now we have our rendered HTML which will look something like this (again with some whitespace added):

<!doctype html>
<html lang="">
  <body>
    <div id="content"><label data-reactid=".e8wbttvlkw" data-react-checksum="-1336527625">
      <input type="checkbox" data-reactid=".e8wbttvlkw.0">
      <span data-reactid=".e8wbttvlkw.1">Write Tutorial</span>
    </label></div>
  </body>
</html>

Finally for the server side, we need to get this rendered markup to the browser:

// file: server/index.js

// [...]

var renderedLayout = // [...]

var app = require('express')();

app.get('/', function(req, res) {
  res.send(renderedLayout);
});

app.listen(3000, function() {
  console.log("Listening on port 3000");
});

Excellent, let's test it out:

$ node server/index.js
Listening on port 3000

Load up your favourite modern browser, and point it to http://localhost:3000, then inspect the source code. You should see the example HTML output as above.

Browser side rendering

Now, let's hook up React on the browser side to re-use the already rendered components!

Let's start with how we expect the browser code to work, and go backwards from there:

// file: browser/index.js
var React = require('react');
var TodoItem = require('../lib/components/todo-item');

// Since we're not using JSX here, we need to wrap the component in a factory
// manually. See https://gist.github.com/sebmarkbage/ae327f2eda03bf165261
var TodoItemFactory = React.createFactory(TodoItem);

var renderTarget = document.getElementById('content')

// Note the identical state to server/index.js
var renderedComponent = React.render(
  TodoItemFactory({done: false, name: 'Write Tutorial'}),
  renderTarget
);

If you're thinking that looks suspiciously like our server side rendering code in server/index.js, you'd be right! This is why React is so powerful - (almost) the same code works both server and browser side. Although, do note the difference that we are using React.render() here (instead of React.renderToString()) which requires us to pass a DOM element as the render target.

But, what about using require in the browser? Browserify to the rescue!

./node_modules/.bin/browserify browser/index.js -d > public/js/bundle.js

(This command can be executed with npm run bundle in the example repo)

Add the generated public/js/bundle.js to the template:

<!-- file: server/templates/layout.handlebars -->
<!doctype html>
<html lang="">
  <body>
    <div id="content">{{{content}}}</div>
    <script src="js/bundle.js"></script>
  </body>
</html>

Now, if we load up our example (with node server/index.js), the HTML will be rendered server side, and React will intelligently pick it up on the browser side without disrupting the DOM (since the state hasn't changed).

Hurray!

Gotchyas

Space is important

In server/templates/layout.handlebars note a lack of space between the <div> and the <label>. This is because when React checks for differences in the DOM, it will see a DOM TextNode consisting of just newline or space characters (e.g; \n or ) as a valid difference, and so will re-render the entire component.

That is to say; given the following rendered DOM string:

<div id="content"><label data-reactid=".e8wbttvlkw" data-react-checksum="-1336527625">...</label></div>

Is considered by React to be different to:

<div id="content">
<label data-reactid=".e8wbttvlkw" data-react-checksum="-1336527625">...</label>
</div>

State change and slow loading Javascript

When the user is on a slow connection (mobile, for example), the public/js/bundle.js script file may take some time to download. During this time, the user is already presented with the form and can begin interacting with the checkbox.

Unfortunately, if the user toggles the checkbox to checked, when React renders the DOM, it will not detect the changed state, instead using the passed in state as the source of truth (as it rightly should).

React has us covered here again with the componentDidMount() method which is executed immediately after rendering, but before returning from the .render() method.

We can leverage this lifecycle method to double check that what's in the DOM and the current state match up:

// file: common/components/todo-item.js
var React = require('react');

module.exports = React.createClass({
  
  // [...]

  componentDidMount: function() {
    this.setState({done: this.refs.done.getDOMNode().checked});
  }

  render: function() {
    return (
      <label>
        <input ref="done" type="checkbox" defaultChecked={this.state.done} />
        {this.props.name}
      </label>
    );
  }
});

We're also using another concept called refs which allow us to directly reference an internal React node within the rendered component. From this, we update the state using the DOM Node's checked attribute.

See Part 3 for unit testing this gotchya.

To emulate this case yourself, you can wrap the contents of browser/index.js in a setTimeout() then inspect the state of renderedComponent.state after rendering. If you toggle the checkbox before the timeout, you should see the component's state has been updated intelligently upon render.

The Checksum id changes

Every time a React component is instantiated then rendered to string, you will receive a different data-react-checksum. This is expected behavior - the browser rendering does not rely solely on this id being the same.

See more in #3.

Part 2

Keep reading Part 2: Unit testing React Components with Mocha + jsdom