• Stars
    star
    165
  • Rank 228,906 (Top 5 %)
  • Language
    JavaScript
  • Created almost 10 years ago
  • Updated over 8 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Isomorphic Server & Browser Side Rendering with React >= 0.12

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

More Repositories

1

react-bsod

🔵 A Blue Screen Of Death (bsod) component to display your errors.
JavaScript
320
star
2

react-testing-mocha-jsdom

Unit testing React Components with Mocha + jsdom
JavaScript
263
star
3

react-workshop

An at-your-own pace practical workshop for absolute beginners to react
252
star
4

aframe-click-drag-component

Aframe Click & Drag Component
JavaScript
96
star
5

extract-to-react

Chrome/Chromium extension for easy HTML to React conversion.
JavaScript
61
star
6

color-svg-sprite

A technique for coloring external .svg files using external .css files.
HTML
59
star
7

node-MarkerWithLabel

npm module of Google Map utility's Marker With Label
JavaScript
39
star
8

aframe-video-billboard

Video Billboard entity & component for A-Frame.
JavaScript
39
star
9

esnext-generation

Learn ES6's Iterators & Generators in this interactive Javascript workshop.
JavaScript
38
star
10

react-testing-isomorphic

Testing Isomorphic React components
JavaScript
37
star
11

pecs

A PICO-8 Entity Component System (ECS) library
Lua
36
star
12

aframe-map

Map entity & component for A-Frame.
JavaScript
35
star
13

birdview-chrome-extension

Get a glance at a whole web page with an aerial view
JavaScript
31
star
14

jsonschemaphp

JSON Schema PHP Validator
PHP
27
star
15

wd15

Web Directions 2015
26
star
16

next-keystone-app

Static Sites with Next.js + KeystoneJS
JavaScript
14
star
17

payment-channels

Bitcoin Payment Channels built on BitPay's libraries for Angel Hack 2014
CoffeeScript
12
star
18

reflux-triggerable-mixin

Reflux stores mixin adding `triggerable` syntax similar to `listenable`.
JavaScript
8
star
19

aframe-frustum-lock-component

Lock an entity to the camera frustum in A-Frame.
JavaScript
8
star
20

emoji-tetra-animated

Generates a gif of the MMO Tetra game hosted by https://twitter.com/EmjoiTetra
JavaScript
7
star
21

cloverfield-build-make

Example using Makefile for cloverfield
JavaScript
7
star
22

wd15-responsive-design

Notes from Vitaly Friedman's Responsive Web Design workshop at Web Directions Sydney 2015
6
star
23

version-changelog

Add a version & URL to your changelog
JavaScript
6
star
24

js-go-presentation

JSConfAU 2016 presentation: How to build Pokemon GO in 100% JS
JavaScript
5
star
25

playcanvas-offline-scripts-server

Enable offline script editing with PlayCanvas Scripts 2.0
JavaScript
5
star
26

relaxql

Relay + GraphQL = RelaxQL: A non-leaky nested component query library
JavaScript
4
star
27

changelog-verify

Verify a changelog has correct entries
JavaScript
3
star
28

pico8-boom-tower

Pico8 infinite tower game
Lua
3
star
29

js-go

AR game in JS
JavaScript
3
star
30

react-checkem

Simple "Select All" for checkboxes in React
JavaScript
3
star
31

node-smallxhr

Very Small XHR library for easy, minimal cross-browser requests
JavaScript
3
star
32

OAuth-PHP

OAuth - Consumer and Server library for PHP
PHP
2
star
33

krapstack.com

KRAP: Keystone + React + APollo
HTML
2
star
34

falcor-shapes-prop-types

Conversion from falcor-shapes to React propTypes
JavaScript
2
star
35

node-encode-decode

A Javascript object to encode and/or decode html characters using HTML or Numeric entities that handles double or partial encoding.
JavaScript
2
star
36

coffee-boilerplate

A quickstart CoffeeScript node server, designed to serve compiled, minified, and source-mapped CoffeeScript modules to the browser.
CoffeeScript
2
star
37

firebase-workshop

Firebase v3 CRUD Workshop
JavaScript
1
star
38

mustache-cache

Compile mustache templates into a requirable, callable function
JavaScript
1
star
39

pico-8-motion-blur

Lua
1
star
40

pico-8-starter

Basic starter template for PICO-8 projects
Lua
1
star
41

random

lambda API to generate a random number from 0-1
JavaScript
1
star
42

pico-8-confetti

Lua
1
star
43

react-list-provider

Use Context to save lists of whatever you want, then access them anywhere in your componpent tree.
JavaScript
1
star
44

sirds-dbpro

SIRDs - Single Image Random Dot Stereogram DLL
C++
1
star
45

slides-intro-to-js

Introduction To JS slide deck
JavaScript
1
star
46

scripts

Scripts to make new projects easier
JavaScript
1
star
47

doubledispatch

A mini tutorial on Double Dispatch (aka: Visitor Pattern)
PHP
1
star
48

keystone-next-adminui

A prototype of using Next.js as KeystoneJS's Admin UI
JavaScript
1
star
49

ga-js1-debugging

General Assembly JS1 Sydney - Lesson 14 - Debugging Exercise
JavaScript
1
star
50

keystone-polymorphism-example

Example of creating polymorphic results from a Keystone API by leveraging GraphQL Union types.
JavaScript
1
star
51

underdb

TypeScript
1
star
52

meetup-attendance

Meetup Attendance taking app
JavaScript
1
star
53

dotfiles

My dotfiles
Shell
1
star
54

js13k-2016

js13k 2016 game competition entry
JavaScript
1
star
55

jes.st

Personal blog
JavaScript
1
star
56

gesture-recognition-dbpro

Discrete Handwriting (Gesture) Recognition
1
star
57

unix-tips

Tips & Tricks for using unix
1
star
58

wallet-sweep

Dogecoin Paper Wallet sweeping made easy via cross platform QR code scanning in the browser.
CoffeeScript
1
star
59

ultimateteamtbc

Site for handling team communication
JavaScript
1
star
60

talk-bet-on-react

My talk "Why I bet my business on React" for NDC Sydney 2018
JavaScript
1
star
61

ga-js1-spa

JS1 Project 3: SPA
CSS
1
star