A simple way to render React on the server

Update (Mar.29, 2018): I now recommend using NextJS to build server-rendered React applications. Read more about it here.


This tutorial will explain the simplest way I have found to server render React components, fetch data, and share component state and props between server and client. You don’t need anything complicated. It’s actually very easy and you can do it with your existing application.

Let’s get started.

Background

Server-rendering your web application provides many benefits such as:

  • Faster perceived page loads, because your JavaScript doesn’t have to finish executing before you see content on the page
  • Search engines will index client-side pages, but cannot crawl them.

If you have a web application that is largely rendered client-side using React, it can be tricky to figure out how to get these benefits.

There are many good tutorials on how to server render React components in your Node web application. Here’s a simple way to do it if you don’t care about data fetching:

import ReactDOMServer from 'react-dom/server';
import express from 'express';

const app = express();

app.get('*', (req, res) => {
    const html = ReactDOMServer.renderToString(<div>Hello World</div>);
    res.send(html);
});

// Code here to run the server.

However, in the real world, we need to fetch a lot of data when rendering our components. That data has to live somewhere like a Flux store, or within a component’s state.

This begs the question: what’s a scalable way to get the benefits of server-side rendering while fetching data on the client? 

Existing tutorials to do this seem more complicated than they should be. I feel like the method below is simpler and easier to understand.

Overview

Here’s an overview of how our server-rendering will work.

  1. Our server-side route handler will create an initialState object. This will store the initial state of our React components. This object can be as simple as {loading: true} or as complex as you wish.
  2. This initialState object will be exposed to the front-end as a global variable using the express-state module.
  3. Our route handler will also render a React component to a string using ReactDOMServer.renderToString().
  4. Our front-end will render the same React component into the same DOM element. It will use the initialState object as props.
  5. After the component renders on the front-end, React will set up the event listeners. We can fetch additional data in componentDidMount() or via Redux.

 

View the complete code

For this tutorial, I’m going to assume you have a few React components and an Express server running. If not, it’s best to follow along using the code available on this repository. Check the README for details.

Install Dependencies

npm i express-state express-handlebars react react-dom babel-register

Apart from the obvious components, we need the following:

  • babel-register: This lets us compile files using Babel on the fly on the server.
  • express-handlebars: Handlebars view engine for Express. You can replace with whatever view engine you like.
  • express-state: Share configuration and state data of an Express app with the client-side via JavaScript.

Build some Components

Let’s build a few simple components. I’m going to build a HomePage component that will act as a container. It will have a subcomponent, Counter, which will just be a simple Counter.

Here’s the code:

var React = require("react");

class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.state = { count: props.count || 1 };
    }

    increment() {
        let newCount = this.state.count + 1;
        this.setState({ count: newCount });
    }

    decrement() {
        let newCount = this.state.count - 1;
        this.setState({ count: newCount });
    }

    componentDidMount() {
        /*
        // Once the component loads, you can fetch some data here, or in a Redux action.
   
        fetch("/api/count")
            .then(r => {
                return r.json();
            })
            .then(data => {
                this.setState({ count: data.count });
            });
        */
    }

    render() {
        return (
            <div>
                <button className="button" onClick={this.decrement.bind(this)}>
                    -
                </button>
                <span>
                    {this.state.count}
                </span>
                <button className="button" onClick={this.increment.bind(this)}>
                    +
                </button>
            </div>
        );
    }
}

export default Counter;
import React from "react";
import Counter from "../components/counter";

class HomePage extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (
            <div>
                <h3 className="title">This is a counter built using React.</h3>
                <Counter count={this.props.count} />
            </div>
        );
    }
}

export default HomePage;

I’ll also add a file called app-client.js. This will be the file that renders these components into the DOM:

// src/app-client.js
import React from "react";
import ReactDOM from "react-dom";
import HomePage from "./pages/home";

window.onload = () => {
    ReactDOM.render(
        <HomePage count={10} />,
        document.getElementById("main")
    );
};

Right now, if we were to run our server, and pull in app-client.js, we would be rendering the React components on the client. The counter would get rendered with an initialState of 10. We haven’t done any server-side rendering yet.


Sign up to my newsletter to be notified about my next post. I only send emails when I write something new.

Setup Server Side Rendering

To set up server-side rendering, we need to modify our Express server a bit. Specifically, let’s set up express-state by adding the following lines where ever you instantiate your Express server:

import expstate from "express-state";

//express state setup
expstate.extend(app);
app.set("state namespace", "MyApp");

Express-state lets us expose data to the client-side. Here, we’re telling our server that any data we expose should live under the MyApp global variable. You can change this to be any variable you want.

Now, let’s update our route handler:

app.get("/", (req, res) => {
    
    // Set up some initial state. This is just an object, 
    // but you could query your database here if you wish.
    const initialState = {
        count: 0,
        isLoaded: false
    };
    
    // Render the component to a string using the initialState.
    const appString = ReactDOMServer.renderToString(<HomePage {...initialState} />);

    //Expose this initialState to your front-end under the MyApp global variable
    res.expose(initialState, "MyApp.initialState");

    // Render this HTML to the page.
    res.render("home", {
        title: "Server Rendered React",
        appString: appString,
        initialState: initialState
    });
});

In our route handler, we are doing the following:

  1. Creating some initial state data.
  2. Rendering our component to an HTML string using the initial state data.
  3. Exposing the initial state data to the front-end.
  4. Rendering out the HTML string on the server-side.

To complete the last step, we need to update our server-side views. In my case, I’m using Handlebars, so I would do something like this in my views/home.handlebars file:

<div id="main">
    {{{appString}}}
</div>

<script>
   {{{state}}}
</script>

{{{appString}}} will get replaced by the unescaped HTML returned by React. {{{state}}} will get replaced by contents of express-state. Importantly, I render my server-side HTML inside the same component that I’ll render my client-side component. This prevents React from re-rendering my component. Instead, it only hooks up event listeners.

Hooking everything up

At this point, everything works, except I haven’t told my client-side React component to look for this initialState that I have set.

So let’s make a final change to app-client.js:

// src/app-client.js
import React from "react";
import ReactDOM from "react-dom";
import HomePage from "./pages/home";

window.onload = () => {
    ReactDOM.render(
        <HomePage {...MyApp.initialState} />,
        document.getElementById("main")
    );
};

Here, I tell my client-side component to look at the values inside MyApp.initialState and pass that object down as props.

Note: In React 16+, you can use ReactDOM.hydrate() instead of ReactDOM.render() for these use cases. More info here

Now, when the page loads:

  1. React gets server-rendered with {counter: 10, loaded: false}.
  2. The client-side picks up the HTML, hooks up the initial state sent by the server, and sets up the event listeners to allow the counter to increment and decrement.
  3. There is no re-rendering. The initial experience is very fast.

Conclusion

In the tutorial above, we saw how we can have React server-render our components and send data to the client. By using a couple useful npm modules, we’re able to have server-rendering working in a scalable way.

Check out the entire repository on GitHub for more information. If you have any thoughts on this, let me know on Twitter or by commenting below.

Couple of Gotchas

Here are some things that I ran into while trying to set this up:

This method works best if you follow the principle of having container and presentational components. I highly recommend this approach.

Ensure your .babelrc has the following presets. This ensures that you can use JSX on the server.

{
    // .babelrc
    "presets": ["es2015", "react"],
    "plugins": ["transform-es2015-arrow-functions"]
}

After updating your .babelrc, you should also require babel-register on your server. Add this to your server.js or equivalent.

#!/usr/bin/env node
require("babel-register")({
    presets: ["react"]
});

Finally, don’t forget to bundle everything using Webpack. Here’s the webpack configuration that I used for this project.

 

  1. Good article, but misses an important point:
    You have to do some fiddling with react-router, you can’t just pass it to SSR without some modifications.

Comments are closed.

Up Next:

Travels through Japan

Travels through Japan