How to Enable Server-Side Rendering for a React App
How to Enable Server-Side Rendering for a React App

Tutorial

How to Enable Server-Side Rendering for a React App

React

Introduction

Server-side rendering (SSR) is a popular technique for rendering a client-side single page application (SPA) on the server and then sending a fully rendered page to the client. This allows for dynamic components to be served as static HTML markup.

This approach can be useful for search engine optimization (SEO) when indexing does not handle JavaScript properly. It may also be beneficial in situations where downloading a large JavaScript bundle is impaired by a slow network.

In this tutorial, you will initialize a React app using Create React App and then modify the project to enable server-side rendering.

At the end of this tutorial, you will have a working project with a client-side React application and a server-side Express application.

Note: Alternatively, Next.js offers a modern approach to creating static and server-rendered applications built with React.

Prerequisites

To complete this tutorial, you will need:

This tutorial was verified with Node v14.4.0 and npm v6.14.5.

Step 1 — Creating the React App and Modifying the App Component

First, we use npx to start up a new React app using the latest version of Create React App.

Let’s call our app my-ssr-app:

  • npx create-react-app@3.4.1 my-ssr-app

Then, we cd into the new directory:

cd my-ssr-app

Finally, we start our new client-side app in order to verify the installation:

  • npm start

You should see an example React app display in your browser window.

Now, let’s create a <Home> component:

  • nano src/Home.js

Next, add the following code to the Home.js file:

src/Home.js
import React from 'react';

export default props => {
  return <h1>Hello {props.name}!</h1>;
};

This will create a <h1> heading with a "Hello" message directed to a name.

Next, let’s render the <Home> in the <App> component. Open the App.js file:

  • nano src/App.js

Then, replace the existing lines of code with these new lines of code:

src/App.js
import React from 'react';
import Home from './Home';

export default () => {
  return <Home name="Sammy" />;
};

This passes along a name to <Home> component so the message that we expect to display is "Hello Sammy!".

In our app’s index.js file, we’ll use ReactDOM’s hydrate method instead of render to indicate to the DOM renderer that we’re rehydrating the app after a server-side render.

Let’s open the index.js file:

  • nano index.js

Then, replace the contents of the index.js file with the following code:

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

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

That concludes setting up the client-side, we can move on to setting up the server-side.

Step 2 — Creating an Express Server and Rendering the App Component

Now that we have our app in place, let’s set up a server that will send along a rendered version. We’ll use Express for our server. Let’s add it to the project by entering the following command in your terminal window:

  • npm install express@4.17.1

Or, using yarn:

  • yarn add express@4.17.1

Next, create a server directory next to the app’s src directory:

  • mkdir server

Then, create a new index.js file that will contain the Express server code:

  • nano server/index.js

Add the imports that will need and define some constants:

server/index.js
import path from 'path';
import fs from 'fs';

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

import App from '../src/App';

const PORT = process.env.PORT || 3006;
const app = express();

Next, add the server code with some error handling:

server/index.js
// ...

app.get('/', (req, res) => {
  const app = ReactDOMServer.renderToString(<App />);

  const indexFile = path.resolve('./build/index.html');
  fs.readFile(indexFile, 'utf8', (err, data) => {
    if (err) {
      console.error('Something went wrong:', err);
      return res.status(500).send('Oops, better luck next time!');
    }

    return res.send(
      data.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
    );
  });
});

app.use(express.static('./build'));

app.listen(PORT, () => {
  console.log(`Server is listening on port ${PORT}`);
});

As you may see, we can import our <App> component from the client app directly from the server.

Three important things are taking place here:

  • We tell Express to serve contents from the build directory as static files.
  • We use a method from ReactDOMServer, renderToString, to render our app to a static HTML string.
  • We then read the static index.html file from the built client app, inject our app’s static content in the <div> with an id of "root", and send that as the response to the request.

Step 3 — Configuring webpack, Babel, and npm Scripts

For our server code to work, we’ll need to bundle and transpile it, using webpack and Babel. To accomplish this, let’s add the dev dependencies to the project by entering the following command in your terminal window:

  • npm install webpack@4.42.0 webpack-cli@3.3.12 webpack-node-externals@1.7.2 @babel/core@7.10.4 babel-loader@8.1.0 @babel/preset-env@7.10.4 @babel/preset-react@7.10.4 --save-dev

Or, using yarn:

  • yarn add webpack@4.42.0 webpack-cli@3.3.12 webpack-node-externals@1.7.2 @babel/core@7.10.4 babel-loader@8.1.0 @babel/preset-env@7.10.4 @babel/preset-react@7.10.4 --dev

Note: An earlier version of this tutorial installed babel-core, babel-preset-env, and babel-preset-react-app. These packages have since been archived, and the mono repo versions are utilized instead.

Next, create a Babel configuration file:

  • nano .babelrc.json

Then, add the env and react-app presets:

.babelrc.json
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ]
}

Note: An earlier version of this tutorial used a .babelrc file (no .json file extension). This was a configuration file for Babel 6, but this is no longer the case for Babel 7.

Now, we will create a webpack config for the server that uses Babel Loader to transpile the code. Start by creating the file:

  • nano webpack.server.js

Then, add the following configurations to the webpack.server.js file:

webpack.server.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  entry: './server/index.js',

  target: 'node',

  externals: [nodeExternals()],

  output: {
    path: path.resolve('server-build'),
    filename: 'index.js'
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      }
    ]
  }
};

With this configuration, our transpiled server bundle will be output to the server-build folder in a file called index.js.

Note the use of target: 'node' and externals: [nodeExternals()] from webpack-node-externals, which will omit the files from node_modules in the bundle; the server can access these files directly.

This completes the dependency installation and webpack and Babel configuration.

Now, we will revisit package.json to add helper npm scripts:

  • nano package.json

We will add dev:build-server, dev:start, and dev scripts to the package.json file to build and serve our SSR application easily:

package.json
"scripts": {
  "dev:build-server": "NODE_ENV=development webpack --config webpack.server.js --mode=development -w",
  "dev:start": "nodemon ./server-build/index.js",
  "dev": "npm-run-all --parallel build dev:*",
  ...
},

We use nodemon to restart the server when we make changes to it. And we use npm-run-all to run multiple commands in parallel.

Let’s install those packages now by entering the following commands in your terminal window:

  • npm install nodemon@2.0.4 npm-run-all@4.1.5 --save-dev

Or, using yarn:

  • yarn add nodemon@2.0.4 npm-run-all@4.1.5 --dev

With this in place, you can run the following to build the client-side app, bundle and transpile the server code, and start up the server on :3006:

  • npm run dev

Or, using yarn:

  • yarn run dev

Our server webpack config will watch for changes and our server will restart on changes. For the client app, however, we currently still need to build it each time we make changes. There’s an open issue for that here.

Now, open http://localhost:3006/ in your web browser and you will see your server-side rendered app.

Previously, the source code revealed:

Output
<div id="root"></div>

But now, with the changes you made, the source code reveals:

Output
<div id="root"><h1 data-reactroot="">Hello <!-- -->Sammy<!-- -->!</h1></div>

The server-side rendering successfully converted the <App> component into HTML.

Conclusion

In this tutorial, you initialized a React application and enabled server-side rendering.

With this post, we just scratched the surface at what’s possible. Things tend to get a bit more complicated once routing, data fetching, or Redux also need to be part of a server-side rendered app.

One major benefit of using SSR is having an app that can be crawled for its content, even for crawlers that don’t execute JavaScript code. This can help with search engine optimization (SEO) and providing metadata to social media channels.

SSR can also often help with performance because a fully loaded app is sent down from the server on the first request. For non-trivial apps, your mileage may vary because SSR requires a setup that can get a bit complicated, and it creates a bigger load on the server. Whether to use server-side rendering for your React app depends on your specific needs and on which trade-offs make the most sense for your use case.

If you’d like to learn more about React, take a look at our How To Code in React.js series, or check out our React topic page for exercises and programming projects.

Creative Commons License