Warning: This tutorial is intended to be a brief introduction to ReactDOM.hydrate()
and ReactDOMServer.rendertoString()
. It is not intended for production use.
Alternatively, Next.js offers a modern approach to creating static and server-rendered applications built with 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.
Prerequisites
To complete this tutorial, you will need:
Node.js installed locally, which you can do by following How to Install Node.js and Create a Local Development Environment.
This tutorial was verified with Node v16.13.1, npm
v8.1.2, react
v17.0.2, @babel/core
v7.16.0, webpack
v4.44.2, express
v4.17.1, nodemon
v2.0.15, and npm-run-all
v4.1.5.
Step 1 — Creating the React App and Modifying the App Component
First, use npx to start up a new React app using the latest version of Create React App.
Let’s call the app, react-ssr-example:
npx create-react-app react-ssr-example
Then, cd
into the new directory:
cd react-ssr-example
Finally, start the new client-side app in order to verify the installation:
npm start
You will observe an example React app displayed in your browser window.
Now, in the src
directory, let’s create a new <Home>
component:
nano src/Home.js
Next, add the following code to the Home.js
file:
src/Home.js
function Home(props) {
return <h1>Hello {props.name}!</h1>;
};
export default Home;
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 in the src
directory:
nano src/App.js
Then, replace the existing lines of code with these new lines of code:
src/App.js
import Home from './Home';
function App() {
return <Home name="Sammy"/>;
}
export default App;
This passes along a name
to <Home>
component so the message that you expect to display is:
"Hello Sammy!"
In the app’s index.js
file, you will use ReactDOM’s hydrate
method instead of render
to indicate to the DOM renderer that you intend to rehydrate the app after a server-side render.
Let’s open the index.js
file in the src
directory:
nano src/index.js
Then, replace the contents of the index.js
file with the following code:
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.hydrate(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
That concludes setting up the client-side, you can move on to setting up the server-side.
Step 2 — Creating an Express Server and Rendering the App Component
Now that you have the app in place, let’s set up a server that will send along a rendered version. You will use Express for the server.
Note: Create React App’s react-scripts
installs a version of express
as requirement for webpack-dev-server
. To avoid dependency tree conflicts, this tutorial no longer includes this installation step.
Next, create a new server
directory in the project’s root directory:
mkdir server
Then, inside of the server
directory, 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 ReactDOMServer from 'react-dom/server';
import express from 'express';
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}`);
});
It is possible to import the <App>
component from the client app directly from the server.
Three important things are taking place here:
Express is used to serve contents from the build
directory as static files.
ReactDOMServer
’s renderToString
is used to render the app to a static HTML string.
The static index.html
file from the built client app is read. The app’s static content is injected into the <div>
with an id
of "root"
. This is sent as a response to the request.
Step 3 — Configuring webpack, Babel, and npm
Scripts
For the server code to work, you will need to bundle and transpile it, using webpack and Babel. To accomplish this.
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 were utilized instead.
Create React App’s react-scripts
handles the installation of the following packages: webpack
, webpack-cli
, webpack-node-externals
, @babel/core
, babel-loader
, @babel/preset-env
, @babel/preset-react
. To avoid dependency tree conflicts, this tutorial no longer includes this installation step.
Next, create a new Babel configuration file in the project’s root directory:
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 (without the .json
file extension). This was a configuration file for Babel 6, but this is no longer the case for Babel 7.
Now, create a webpack config for the server that uses Babel Loader to transpile the code. Start by creating the webpack.server.js
file in the project’s root directory:
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, the 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, revisit package.json
and add helper npm
scripts:
nano package.json
Add dev:build-server
, dev:start
, and dev
scripts to the package.json
file to build and serve the SSR application:
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:*",
// ...
},
The dev:build-server
script sets the environment to "development"
and invokes webpack
with the configuration file you created earlier. The dev:start
script invokes nodemon
to serve the built output.
The dev
script invokes npm-run-all
to run in parallel
the build
script and all scripts that start with dev:*
– including dev:build-server
and dev:start
.
Note: You do not need to modify the existing "start"
, "build"
, "test"
, and "eject"
scripts in the package.json
file.
nodemon
is used to restart the server when changes are made. npm-run-all
is used 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.15 --save-dev
npm install npm-run-all@4.1.5 --save-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
The server webpack config will now watch for changes and the server will restart on changes. For the client app, however, will require to be manually rebuilt each time a change is made.
Note: There’s an open issue for that here.
Now, open http://localhost:3006/
in your web browser and observe your server-side rendered app.
Previously, viewing the source code revealed:
<div id="root"></div>
But now, with the changes you made, the source code reveals:
<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.