Log In

React server side rendering


by Yariv Katz

Our job in this tutorial is to learn how to implement Server side rendering within our react app. We would like to bootstrap our react app using facebook's create-react-app cli. And add to that bootstrap server side rendering along with support for different routes, and also passing data from the server part to the browser client part.

Problems of SPA

First let's explain what exactly is server side rendering or in short SSR. By default when we create our react application using create-react-app, Our index.html file will look similar to this:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <link rel="shortcut icon" href="/favicon.ico">
        <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
        <meta name="theme-color" content="#000000">
        <link rel="manifest" href="/manifest.json">
        <title>React App</title>
        <link href="/static/css/main.d52c2f7b.chunk.css" rel="stylesheet">
    </head>
    <body>
        <noscript>You need to enable JavaScript to run this app.</noscript>
        <div id="root">

        </div>
        <script>
            // ...
        </script>
        <script src="/static/js/1.2dd891c4.chunk.js"></script>
        <script src="/static/js/main.f7ebf427.chunk.js"></script>
    </body>
</html>

Notice that the html has very little content describing our web application. Our web application is using js to draw the web pages. This could present problems in two aspects:
- Slower initial load
- Poor Search engine optimization.

slow initial load

To demonstrate why we have with a minimal index, slow initial load of our app, let's examine an extreme case like the following:
Let's say our application js files size is 10MB, let's also say that we are in a place with a slow internet connection and it takes us 30 seconds to download 10mb of files and we process the downloaded files with a slow mobile phone. In this case our phone will ask for the site, we will get the index.html and still see a blank screen or a loading sign and then start downloading the files finish downloadiong the files after 30 seconds and then process them and only then will the site be loaded. Meaning it took more than 30 seconds to show our site, according to statistics regarding user behaviour, we will lose more than 50% of our users if our site will not load in less then 3 seconds.

poor SEO

Our web application will get a grade from google for every search term. In order for our site to be graded, google will have to render the site with the search bots. The search bots will iterate over web sites and render them and grade them according to the content of those sites along with different links that lead to that site. When we present an empty index.html that contains no content, we basically asking the bot to grade our site based on the rendered result after downloading and processing the js files. Google bot will actually do that, and download the js files, run them, and render our site after running the js files (other search bots are not running the js). Still there is a problem with us relying on how google will render our js files:
- We can't know for sure that the rendering was ok, google might run in some sort of js exception and render our site poorly.
- For the time it takes the bot to download and render our site we will be punished since google grades us for the loading time of our site as well.

What is SSR

The new frontend frameworks and libraries like React and the new Angular, can be run on the server side using node. What does it mean to run frontend on a server, the result on the browsers is kinda obvious, react component are translated to a change in the DOM tree and after rendering we can view our components in the DOM tree, but the server does not have a DOM tree it's something that happens only in the browser. By running our react code on the server, the server can produce the HTML representation of our react components. This means the the index.html we can produce with SSR can represent a full HTML of our site. With SSR the HTML is full of content and after our js files are loaded, the content is changed to what to js files render, and from that point our site becomes SPA. This means that the initial load will be fast cause we present a full html and the user sees the first image of the site with the initial HTML. The SEO is also better since the bots are getting the full html and do not require to render the js files. Our job in this tutorial will be to implement SSR with React.

Bootstrap React

Let's start with a new react project started with the create-react-app. In your terminal type:

> npx create-react-app react-ssr

Few things to note when starting a new react app with the cli. Notice that the entry point js file index.js is using ReactDOM. ReactDOM is a library meant to connect react to a browser DOM, this means that this entry point file we can't run on our server side. Instead we will need to add a different webpack configuration file that has an entry point for the server side. Add the file webpack.server.config.js with the following:

const path = require('path')

module.exports = {
    entry: './src/server.js',
    output: {
        path: path.resolve(__dirname, 'build'),
        filename: 'server.js'
    },
    mode: 'development',
    target: 'node',
    devtool: 'sourcemap',
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            ["@babel/react"]
                        ]
                    }
                },
                exclude: [/node_modules/],
            },
            {
                test: /\.(svg|css)$/,
                use: 'null-loader'
            }
        ]
    }
}

we configured a webpack for our server side. The entry point file we will create soon enough and it will be named server.js and will reside in the src folder. we will target webpack to compile the code to node compatible code. We also instructed the server webpack he can ignore the imported image or css files since only the client will need to process them. We will process the js files using @babel/react which we will need to install and the null-loader as well:

> npm install @babel/react --save-dev
> npm install null-loader --save-dev

Let's verify that everything is working properly and create in the src folder a file called server.js with the following simple react component:

import React, {Component} from 'react';

console.log('hello from server');

export default class App extends Component {
    render() {
        return <h1>Hello world</h1>
    }
}

Now to instruct webpack to compile our code we run in the terminal:

> webpack --config webpack.server.config.js

this will output a file in the build folder we can run with node:

> node build/server.js

this should print an hello from server message.

express and renderToString

Our server job is quite simple. we will grab the index.html file created when building our client project. we will then call renderToString on the react-dom/server library and render our app component, this will produce an html of our app. We will place that html in the proper container in the index.html we created. We will use express as our server framework. Install express:

> npm install express --save

Now modify the file src/server.js

import React from 'react';
import * as express from 'express';
import {renderToString} from 'react-dom/server';
import App from './App';
import {readFile} from 'fs';
import {promisify} from 'util';
import * as cheerio from 'cheerio';
import * as path from 'path';

const app = express();

app.use('/static', express.static(path.resolve(process.cwd(), 'build/static')));

app.get('*', async function(req, res) {
    const appHtml = renderToString(<App />);
    const readFilePromise = promisify(readFile);
    const html = await readFilePromise(path.resolve(process.cwd(), 'build/index.html'))
    const $ = cheerio.load(html);
    $('#root').html(appHtml)
    res.send($.html());
})

app.listen(3000, function() {
    console.log('now listening on port 3000');
});

Few intresting things here:
- We are loading the static middleware to serve the static files created from the client build. For every static url we will present a static file. Note that it is better to deal with static files with a CDN and not using express.
- We are rendering our root react component, and creating html from that component. We are then reading the client html file and using cheerio which is similar to jquery on the server side we are placing our html in the proper div. We are then sending that html.
We need to install cheerio:

> npm install cheerio --save

Now if building the server and running the server file with node you should see you app in the browser loaded with a full html.

ReactDOM.hydrate

Our client is switching the container div with the js content. For faster rendering we can instruct react that the server populated the container and we just need the changes and not a full render. We are doing this with ReactDOM.render. Modify the index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.hydrate(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

react router

proper server side rendering should render the proper page according to the url. This means that if our app is using a router and we request a page, the html rendered by the server should match the component that will be rendered on screen. Let's install the router:

> npm install react-router-dom --save

We will create a home page and an about page and make sure the html from server on the url of the about page is matching the about page. In the src folder create the file Home.js with the following:

import React, {Component} from 'react';

export default class Home extends Component {
    render() {
        return <h1>Home Page</h1>
    }
}

Similar to the home page create the file About.js with the following:

import React, {Component} from 'react';

export default class About extends Component {
    render() {
        return <h1>About Page</h1>
    }
}

Now let's use the router to create a route for those two pages. Modify the App.js with the following:

import React, { Component } from 'react';
import Home from './Home';
import About from './About';
import './App.css';
import { Switch, Route } from 'react-router-dom';

class App extends Component {
    render() {
        return (
        <div className="App">
            <Switch>
            <Route exact path="/" component={Home} />
            <Route path="/about" component={About} />
            </Switch>
        </div>
        );
    }
}

export default App;

We will have to modify the index.js and wrap our app in the router for the client.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import {BrowserRouter} from 'react-router-dom';

ReactDOM.hydrate(<BrowserRouter><App /></BrowserRouter>, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();

we wrap the browser part of the code with the BrowserRouter. Now in the server side of the code. Modify the server.js with the following:

import React from 'react';
import * as express from 'express';
import {renderToString} from 'react-dom/server';
import App from './App';
import {readFile} from 'fs';
import {promisify} from 'util';
import * as cheerio from 'cheerio';
import * as path from 'path';
import {StaticRouter} from 'react-router-dom';

const app = express();

app.use('/static', express.static(path.resolve(process.cwd(), 'build/static')));

app.get('*', async function(req, res) {
    const context = {};
    const appHtml = renderToString(<StaticRouter location={req.url} context={context}><App /></StaticRouter>);
    if (context.url) {
        res.redirect(context.url);
    }
    const readFilePromise = promisify(readFile);
    const html = await readFilePromise(path.resolve(process.cwd(), 'build/index.html'))
    const $ = cheerio.load(html);
    $('#root').html(appHtml)
    res.send($.html());
})

app.listen(3000, function() {
    console.log('now listening on port 3000');
});

The code is pretty similar, this time in the renderToString we are wrapping the App component with the proper StaticRouter and passing the url of the request. we are also passing an object in case we get a redirect from the router we can redirect the user from the backend. Try to build the client and then the server and you should see the proper html is rendered according to the route.

Summary

Server side rendering is important to implement and unfortunatly is forgotten in many spa's. When implementing SSR we have to remember to keep our components universal and when dealing with DOM stuff we have to make sure we are running on the browser. SSR is also not that difficult to implement, so it is a bit of overkill to go with the opinionated starterkits of react only for that reason. Hope this article was helpful. Happy coding.