Server Side Rendering with React and Redux

Artem Mirchenko
9 min readApr 29, 2018

--

This tutorial will give you basic understanding how to build React components, collect Redux store on server and make it usable in browser.

This guide is aimed at those who have basic knowledge of React and NodeJs.

Pre requirements:

  • Installed Node.js
  • Basic knowledge of JavaScript.
  • npm or yarn

Getting started

As this guide focused exceptionally on ssr , i offering you to clone my github react project https://github.com/mirchenko/react-ssr-boilerplate. This is react boilerplate only, without server side rendering.

Once downloading complete run install command yarn or npm install

After that, run yarn start command and navigate to localhost:8080

If you see something like this, everything is ok and you installed project on the right way. I leave link to countries api https://restcountries.eu/rest/v2 , so you can feel free to play with.

The Problem

Move to the console and see React warning message.

Then switch to the Network tab and choose localhost on the left bar.

The problem is that content generated locally by react does not match to content returned by server. So necessary to write server which will return original content.

Server

For correct handling ES6 features on servers bundle set up webpack server configuration.

Install externals plugin for webpack and express server library.

yarn add webpack-node-externals express

Create webpack.server.config.js in root of the project and put some code in there.

const path = require('path');
const webpackNodeExternals = require('webpack-node-externals');

module.exports = {
mode: 'development',
target: 'node',
entry: './server/index.js',
output: {
path: path.resolve(__dirname),
filename: 'server.js'
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/
}
]
},
externals: [webpackNodeExternals()]
};

Put server bundling script to the package.json file.

"scripts": {
"start": "webpack-dev-server",
"build": "webpack --config webpack.build.config.js",
"server-build": "webpack --config webpack.server.config.js"
},

Create server directory in the root of the project, then create index.js file in there. Set up your server in server/index.js file.

import express from 'express';

const PORT = 8079;
const app = express();

app.listen(PORT, () => console.log(`Frontend service listening on port: ${PORT}`));

Run react bundle build command at first:

yarn build

Then run server bundling command:

yarn server-build

Once this command complete you should see server.js file in the root of your project. Run server by node js.

node server.js

Look at the terminal, you should see console message from your server.

Frontend service listening on port: 8079

Time to add route handler to server/index.js file.

import express from 'express';

const PORT = 8079;
const app = express();


app.get('*', (req, res) => {
const raw =`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<div id="app"></div>
<script src="dist/bundle.js"></script>
</body>
</html>
`;

res.send(raw);
});

app.listen(PORT, () => console.log(`Frontend service listening on port: ${PORT}`));

Rebuild server and start again, switch to the browser and you will see raw content returning by the server.

But there is no fetched countries there, because when browser calls scripts /dist/bundle.js server does not understand thats, and on each route server returning content. Fix that using express.static

import express from 'express';

const PORT = 8079;
const app = express();

app.use('/dist', express.static('dist'));
app.use('/img', express.static('img'));

app.get('*', (req, res) => {
const raw =`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<div id="app"></div>
<script src="dist/bundle.js"></script>
</body>
</html>
`;

res.send(raw);
});

app.listen(PORT, () => console.log(`Frontend service listening on port: ${PORT}`));

Switch to the browser and see countries.

But in server response #app container still empty, and warning massage on console still present.

As we can not use BrowserRouter on server side, we will use StaticRouter . Also we have same set up as frontend, but wrap it all by renderToString function from react-dom/server library.

import React from 'react';
import express from 'express';
import { Provider } from 'react-redux';
import { StaticRouter } from 'react-router-dom';
import { renderToString } from 'react-dom/server';
import store from '../src/store';
import Routes from '../src/router/Routes';

const PORT = 8079;
const app = express();

app.use('/dist', express.static('dist'));
app.use('/img', express.static('img'));

app.get('*', (req, res) => {
const content = renderToString(
<Provider store={store}>
<StaticRouter>
<Routes />
</StaticRouter>
</Provider>
);

const raw =`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<div id="app">${content}</div>
<script src="dist/bundle.js"></script>
</body>
</html>
`;

res.send(raw);
});

app.listen(PORT, () => console.log(`Frontend service listening on port: ${PORT}`));

To make ease working with raw render features, create server/render.js file and past all render things to it.

server/render.js file should looks like this:

import React from 'react';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
import { StaticRouter } from 'react-router-dom';
import Routes from '../src/router/Routes';

export default store => {
const content = renderToString(
<Provider store={store}>
<StaticRouter>
<Routes />
</StaticRouter>
</Provider>
);


return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<div id="app">${content}</div>

<script src="dist/bundle.js"></script>
</body>
</html>
`;
};

server/index.js file should looks like this:

import express from 'express';
import render from './render';
import store from '../src/store';

const PORT = 8079;
const app = express();

app.use('/dist', express.static('dist'));
app.use('/img', express.static('img'));

app.get('*', (req, res) => {
const content = render(store);

res.send(content);
});

app.listen(PORT, () => console.log(`Frontend service listening on port: ${PORT}`));

Add store object to raw content and stringify it before.

import React from 'react';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
import { StaticRouter } from 'react-router-dom';
import Routes from '../src/router/Routes';

export default store => {
const content = renderToString(
<Provider store={store}>
<StaticRouter>
<Routes />
</StaticRouter>
</Provider>
);


return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<div id="app">${content}</div>
<script>
window.INITIAL_STATE = ${JSON.stringify(store.getState())}
</script>
<script src="dist/bundle.js"></script>
</body>
</html>
`;
};

Build and run server again, navigate to browser, look at localhost on Network tab and see store object and rendered content returned from server.

Error in console gone missing. you can see html content, but .countries-container is empty. To fix that necessary to handle fetching inside components on server. But before handling fetching need to completely rewrite routes.

Install react-router-config library at first:

yarn add react-router-config

Once installation finished move to /src/router/Routes.js file and set up routes schema.

import Countries from "../components/Countries";
import Country from "../components/Country";


export default [
{
component: Countries,
path: '/',
exact: true
},
{
component: Country,
path: '/:name'
}
];

Next go to the /src/router/index.js file and modify it to server routes from router schema:

import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
import Routes from './Routes';

export default () => {
return (
<BrowserRouter>
<div>{renderRoutes(Routes)}</div>
</BrowserRouter>
);
};

You can stop server and run frontend by command yarn start to make sure that new router definition works fine.

Move to to server/render.js file and do same thing with router such as frontend.

import React from 'react';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
import { StaticRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
import Routes from '../src/router/Routes';

export default (pathname, store, context) => {
const content = renderToString(
<Provider store={store}>
<StaticRouter location={pathname} context={context}>
<div>{renderRoutes(Routes)}</div>
</StaticRouter>
</Provider>
);


return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<div id="app">${content}</div>
<script>
window.INITIAL_STATE = ${JSON.stringify(store.getState())}
</script>
<script src="dist/bundle.js"></script>
</body>
</html>
`;
};

Rebuild and run server to make sure there is no errors. Than look at your terminal and see context warning:

To fix that and finish work with server/render.js file pass context object and pathname to it StaticRouter

import React from 'react';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
import { StaticRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
import Routes from '../src/router/Routes';

export default (pathname, store, context) => {
const content = renderToString(
<Provider store={store}>
<StaticRouter location={pathname} context={context}>
<div>{renderRoutes(Routes)}</div>
</StaticRouter>
</Provider>
);


return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<div id="app">${content}</div>
<script>
window.INITIAL_STATE = ${JSON.stringify(store.getState())}
</script>
<script src="dist/bundle.js"></script>
</body>
</html>
`;
};

Do not forget pass it from server/index.js

import express from 'express';
import render from './render';
import store from '../src/store';

const PORT = 8079;
const app = express();

app.use('/dist', express.static('dist'));
app.use('/img', express.static('img'));

app.get('*', (req, res) => {
const context = {};
const content = render(req.path, store, context);

res.send(content);
});

app.listen(PORT, () => console.log(`Frontend service listening on port: ${PORT}`));

Rebuild and rerun server, context warning message should be disappear.

Fetching

Move to src/components/Countries/index.js file, look at Countries component. To make easy fetching from on server add static fetching function to it. Server will only get static fetching method from this class, handle it and attach result of this method to store. After that server will pass full of data store object to the client.

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchCountries } from "../../action/countries";
import { Loading } from '../../common';
import CountriesItem from './CountriesItem';

const m = ({ countries }) => ({ countries });


@connect(m, { fetchCountries })
export default class Countries extends Component {

static fetching ({ dispatch }) {
return [dispatch(fetchCountries())];
}

componentDidMount() {
this.props.fetchCountries();
}


render() {
const { countries: { isFetching, data } } = this.props;

if(isFetching) {
return <Loading />
}

return(
<div className="container">
<div className="countries-container">
{data.map((item, i) => <CountriesItem key={i} {...item} />)}
</div>
</div>
);
}
};

fething method returns array because initial actions can be more then 1. Each of element can returning promise, so server should handle all the promises from all components returned from StackRouter.

Move to server/index.js file and handle all the promises.

import express from 'express';
import { renderToString } from 'react-dom/server';
import { matchRoutes } from 'react-router-config';
import render from './render';
import store from '../src/store';
import Routes from '../src/router/Routes';


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

app.use('/dist', express.static('dist'));
app.use('/img', express.static('img'));
app.get('*', async (req, res) => {


const actions = matchRoutes(Routes, req.path)
.map(({ route }) => route.component.fetching ? route.component.fetching(store) : null)
.map(async actions => await Promise.all(
(actions || []).map(p => p && new Promise(resolve => p.then(resolve).catch(resolve)))
)
);

await Promise.all(actions);
const context = {};
const content = render(req.path, store, context);

res.send(content);
});

app.listen(PORT, () => console.log(`Frontend service listening on port: ${PORT}`));

After all promises will handled, server will return full of data store, and full of content html raw. Rebuild and rerun server, switch to the browser. Open Network tab, click on localhost on the left bar.

Ssr works fine. But what about single country? If you navigate to http://localhost:8079/United%20States%20of%20America server returns only html mok for this country, but not the data.

To fix that move to src/components/Country/index.js , look at componentDidMount hook, you will see this.props.fetchCountry(this.props.match.params.name); . Necessary to pass country name somehow. Remember that, but now just write static fetching method in this class.

static fetching ({ dispatch, path }) {
return [dispatch(fetchCountry(path.substr(1)))];
}

That pass pathname to fetching method move to server/index.js and destruct store object and pass req.path to it.

import express from 'express';
import { renderToString } from 'react-dom/server';
import { matchRoutes } from 'react-router-config';
import render from './render';
import store from '../src/store';
import Routes from '../src/router/Routes';


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

app.use('/dist', express.static('dist'));
app.use('/img', express.static('img'));
app.get('*', async (req, res) => {


const actions = matchRoutes(Routes, req.path)
.map(({ route }) => route.component.fetching ? route.component.fetching({...store, path: req.path }) : null)
.map(async actions => await Promise.all(
(actions || []).map(p => p && new Promise(resolve => p.then(resolve).catch(resolve)))
)
);

await Promise.all(actions);
const context = {};
const content = render(req.path, store, context);

res.send(content);
});

app.listen(PORT, () => console.log(`Frontend service listening on port: ${PORT}`));

Rebuild and rerun server and you will se raw html with data, and country data in store returned from server.

Done

This is easiest way setting up server side rendering with react and redux. I leave link to finished sample https://github.com/mirchenko/react-ssr

If yo have any questions or propositions feel free to ask responses.

Thank for reading.

--

--