Server-side rendering in React
Originally when Single Page Applications weren’t a thing every page was a different HTML returned from a response to a HTTP call. Either a static content file or by processing the response via server side languages (such as PHP, Python or Java) and responding in a more dynamic way.
SPAs allowed us to deliver responsive sites that work a lot faster than standard request-response model by removing the “request travel time”.
Typical response from server when accessing a React website would be something like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="/favicon.ico">
<title>My React app</title>
</head>
<body>
<div id="root"></div>
<script src="/app.js"></script>
</body>
</html>
After getting this document our browser also fetches the app.js
which contains our application’s javascript. Before our browser loads our javascript all we can see trought the inspector tool is this HTML output.
Why is that a problem?
- Slow internet connection might not load
app.js
quickly enought to have a good user experience - Slow devices, like older smartphones might not have the fastest loading times on the javascript code
- Search engines and social networks might not recognise & find your website content as it is being loaded in client-side
How to solve this problem
1. Having your key pages static
If you have a website where your users can login, and can’t access the content not-signed you might decide to create public sites (like the index, “about us”, “contact us” etc.) pages as static HTML, and not have them rendered by JS.
Since your content is restricted by login requirements it will not be indexed by search engines or shared in social media.
2. Generate parts of your application as HTML pages when running the build process
Libraries such as react-snapshot can be used to generate HTML copies of your application pages and save them to a specified folder. The folder then can be deployed alongside your other assets. In this manner, the HTML will be served together with the response allowing your site to be accessible by users with JavaScript disabled, search engines, social networks etc.
In most of the cases, configuration of react-snapshot is as straightforward as installing the dependency into your project, and creating a new script:
"build": "webpack && react-snapshot --build-dir static"
The disadvantage of this solution is that all the content we want to generate must be available at build time — we can’t query any APIs to get it, we can’t pre-generate content that depends on user provided data (e.g. as URL).
3. Create a server-side rendered application in JS
One of the appealing aspects of the new generation of javascript web applications is that they can be ran on both the client (browser) and on server — this allows us to generate HTML for pages that are more dynamic — which content is not known at build time. This is often referred to as “isomorphic” or “universal” application.
Two of the most popular solutions that provide SSR for React are:
- next.js — https://github.com/zeit/next.js/
- Gatsby — https://github.com/gatsbyjs/gatsby
Create your own custom SSR solution
You’re going to need to be able to run a node backend for your server. You can not deploy this solution to static host like github pages.
First step we’re going to take is to create an application just like you would with any other React application.
First, we will need to add three packages:
yarn add express babel-node pug --save-dev
- Express is the most popular webserver for node.
- babel-node is a wrapper for node, which allows it to perform code transpilation on the fly
- pug is a templating engine we can use with express.
First, we make a copy of our index.html, and save it as index.pug:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<div id="root">!{app}</div>
<script src="bundle.js"></script>
</body>
</html>
You can find only one difference in our pug file. It is the !{app}
pug variable which will be converted into HTML.
Our server will look like this
import React from 'react';
import { renderToString } from 'react-dom/server';
import express from 'express';
import path from 'path';
import App from './src/App';const app = express();
app.set('view engine', 'pug');
app.use('/', express.static(path.join(__dirname, 'dist')));
app.get('*', (req, res) => {
const html = renderToString(
<App />
);
res.render(path.join(__dirname, 'src/index.pug'), {
app: html
});
});app.listen(3000, () => console.log('listening on port 3000'));
Let’s see what each line of code does.
import { renderToString } from 'react-dom/server';
react-dom contains a named export renderToString
which works in a similar fashion to the render
, but instead of rendering to the DOM, it renders the HTML in string format.
const app = express();
app.set('view engine', 'pug');
app.use('/', express.static(path.join(__dirname, 'dist')));
A new express server instance is created here and it is being configured to use the view engine of pug
.
Then we’re instructing express to search for the file in the dist
folder, and if a request (e.g. /bundle.js) matches a file, that is present in that folder, respond with it.
app.get('*', (req, res) => {});
Now in turn we ask express to add a handler for any unmatched URL — this includes our non-existing index.html
file (remember, we renamed it to index.pug
and also it’s not available in the dist
folder).
const html = renderToString(
<App />
);
Using renderToString
we render our application. This code looks identical to our entry point, but it isn’t required to be so.
res.render(path.join(__dirname, 'src/index.pug'), {
app: html
});
Once we have the rendered HTML, we tell express to respond by rendering the index.pug
file, and replacing the app
variable, with the HTML we received.
app.listen(3000, () => console.log('listening on port 3000'));
And we are starting the server!
All that’s left to do is to add a proper script topackage.json
:
"scripts": {
"server": "babel-node server.js"
}
Run yarn run server
and we can see our server is running. Navigate your browser to http://localhost:3000
where you should again see your application. But this time if you view the source you should see something like this:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<div id="root">div data-reactroot="">
Contents of App.js
</div>
<script src="bundle.js"></script>
</body>
</html>
Things to have in mind
While SSR mainly looks straightforward, you need to watch out for some topics, which might not be initially clear:
- React component lifecycles are not not called on the server (
componentDidMount)
it means that fetched data that you are rendering on runtime, won’t be SSR; this is generally a good thing, as you should be providing the data as props if you need it. Remember that you need to delay your render (callingres.render
) until the data is fetched, which might introduce some delay for your visitors - any state generated on the server side will not be passed to the client application state; that means that if your backend fetches some data and uses it to render HTML, it will not be placed in the
this.state
that the browser sees - if you’re using a react-router you need to make sure that the proper URL is passed to the application when it’s rendered on the server.