This tutorial is out of date and no longer maintained.
Now that we’ve had a look at a basic setup for React server-side rendering (SSR), let’s crank things up a notch and look at how to use React Router v4 on both the client and the server. After all, most real apps need routing, so it only makes sense to learn about setting up routing so that it works with server-side rendering.
We’ll start things where we left things up in our intro to React SSR, but on top of that setup we’ll also need to add React Router 4 to our project:
- yarn add react-router-dom
Or, using npm:
- npm install react-router-dom
And next, we’ll set up a simple routing scenario where our components are static and don’t need to go fetch external data. We’ll then build on that to see how we would set things up for routes that do some data fetching on rendering.
On the client-side, let’s simply wrap our App
component with React Router’s BrowserRouter
component, as usual:
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
ReactDOM.hydrate(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
And then on the server, we’ll use the analogous, but stateless StaticRouter
component:
import React from 'react';
import express from 'express';
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
// ...other imports and Express config
app.get('/*', (req, res) => {
const context = {};
const app = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
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.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
The StaticRouter
component expects a location
and a context
prop. We pass the current URL (Express’ req.url
) to the location
prop and an empty object to the context
prop. The context
object is useful to store information about a specific route render, and that information is then made available to the component in the form of a staticContext
prop.
To test that everything is working as we would expect, let’s add some routes to our App
component:
import React from 'react';
import { Route, Switch, NavLink } from 'react-router-dom';
import Home from './Home';
import Posts from './Posts';
import Todos from './Todos';
import NotFound from './NotFound';
export default props => {
return (
<div>
<ul>
<li>
<NavLink to="/">Home</NavLink>
</li>
<li>
<NavLink to="/todos">Todos</NavLink>
</li>
<li>
<NavLink to="/posts">Posts</NavLink>
</li>
</ul>
<Switch>
<Route
exact
path="/"
render={props => <Home name="Alligator.io" {...props} />}
/>
<Route path="/todos" component={Todos} />
<Route path="/posts" component={Posts} />
<Route component={NotFound} />
</Switch>
</div>
);
};
Note: We’re making use of the Switch component to render only one matching route.
Now if you test out this setup (yarn run dev
), you’ll see that everything is working as expected and our routes are being server-side rendered.
We can improve on things a little bit and serve the content with an HTTP status code of 404 when rendering the NotFound
component. First, here’s how we can attach some data to the staticContext
in the NotFound
component:
import React from 'react';
export default ({ staticContext = {} }) => {
staticContext.status = 404;
return <h1>Oops, nothing here!</h1>;
};
Then, on the server, we can check for a status of 404 on the context
object and serve the file with a status of 404 if our check evaluates to true:
// ...
app.get('/*', (req, res) => {
const context = {};
const app = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
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!');
}
if (context.status === 404) {
res.status(404);
}
return res.send(
data.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
);
});
});
// ...
As a side note, you can do something similar to deal with redirects. React Router automatically adds an url
property with the redirected URL to the context object when a Redirect
component is used:
if (context.url) {
return res.redirect(301, context.url);
}
In the case where some of our app’s routes need to load data upon rendering, we’ll need a static way to define our routes instead of the dynamic way of doing it when only the client is involved. Losing the ability to define dynamic routes is one reason why server-side rendering is best kept for apps that really need it.
Since we’ll be using fetch on both the client and the server, let’s add isomorphic-fetch to the project. We’ll also add the serialize-javascript
package, which will be handy to serialize our fetched data on the server:
- yarn add isomorphic-fetch serialize-javascript
Or, using npm:
- npm install isomorphic-fetch serialize-javascript
Let’s define our routes as a static array in a routes.js
file:
import App from './App';
import Home from './Home';
import Posts from './Posts';
import Todos from './Todos';
import NotFound from './NotFound';
import loadData from './helpers/loadData';
const Routes = [
{
path: '/',
exact: true,
component: Home
},
{
path: '/posts',
component: Posts,
loadData: () => loadData('posts')
},
{
path: '/todos',
component: Todos,
loadData: () => loadData('todos')
},
{
component: NotFound
}
];
export default Routes;
Some of our routes now have a loadData
key that points to a function that calls a loadData
function. Here’s our implementation for loadData
:
import 'isomorphic-fetch';
export default resourceType => {
return fetch(`https://jsonplaceholder.typicode.com/${resourceType}`)
.then(res => {
return res.json();
})
.then(data => {
// only keep 10 first results
return data.filter((_, idx) => idx < 10);
});
};
We’re simply using the fetch API to get some data from a REST API.
On the server, we’ll make use of React Router’s matchPath
to find the current route and see if it has a loadData
property. If that’s the case, we call loadData
to get the data and add it to the server’s response using a variable attached to the global window
object:
import React from 'react';
import express from 'express';
import ReactDOMServer from 'react-dom/server';
import path from 'path';
import fs from 'fs';
import serialize from 'serialize-javascript';
import { StaticRouter, matchPath } from 'react-router-dom';
import Routes from '../src/routes';
import App from '../src/App';
const PORT = process.env.PORT || 3006;
const app = express();
app.use(express.static('./build'));
app.get('/*', (req, res) => {
const currentRoute =
Routes.find(route => matchPath(req.url, route)) || {};
let promise;
if (currentRoute.loadData) {
promise = currentRoute.loadData();
} else {
promise = Promise.resolve(null);
}
promise.then(data => {
// Let's add the data to the context
const context = { data };
const app = ReactDOMServer.renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
const indexFile = path.resolve('./build/index.html');
fs.readFile(indexFile, 'utf8', (err, indexData) => {
if (err) {
console.error('Something went wrong:', err);
return res.status(500).send('Oops, better luck next time!');
}
if (context.status === 404) {
res.status(404);
}
if (context.url) {
return res.redirect(301, context.url);
}
return res.send(
indexData
.replace('<div id="root"></div>', `<div id="root">${app}</div>`)
.replace(
'</body>',
`<script>window.__ROUTE_DATA__ = ${serialize(data)}</script></body>`
)
);
});
});
});
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
Notice how we now also add the component’s loaded data to the context object. We’ll access this from staticContext
when rendering on the server.
Now, in our components that need to fetch data on load, we can add some simple logic to their constructor and their componentDidMount
lifecycle method:
Here’s an example with our Todos
component:
import React from 'react';
import loadData from './helpers/loadData';
class Todos extends React.Component {
constructor(props) {
super(props);
if (props.staticContext && props.staticContext.data) {
this.state = {
data: props.staticContext.data
};
} else {
this.state = {
data: []
};
}
}
componentDidMount() {
setTimeout(() => {
if (window.__ROUTE_DATA__) {
this.setState({
data: window.__ROUTE_DATA__
});
delete window.__ROUTE_DATA__;
} else {
loadData('todos').then(data => {
this.setState({
data
});
});
}
}, 0);
}
render() {
const { data } = this.state;
return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>;
}
}
export default Todos;
When rendering on the server, we can access the data from props.staticContext.data
because we’ve put in into StaticBrowser’s context object.
There’s a little bit more logic going on with the componentDidMount
method. Remember that this method is only called on the client. If __ROUTE_DATA__
is set on the global window object it means that we’re rehydrating after a server render and we can get the data directly from __ROUTE_DATA__
and then delete it. If __ROUTE_DATA__
is not set, then we arrived on that route using client-side routing, the server is not involved at all and we need to go ahead and fetch the data.
Another interesting thing here is the use of a setTimeout
with a delay value of 0ms. This is just so that we can for the next JavaScript tick to ensure that __ROUTE_DATA__
is available.
There’s a package available and maintained by the React Router team, React Router Config, that provides two utilities to make dealing with React Router and SSR much easier: matchRoutes
and renderRoutes
.
The routes in our previous example are quite simplistic and there are no nested routes. In cases where multiple routes may be rendered at the same time, using matchPath
won’t work because it’ll only match one route. matchRoutes
is a utility that helps match multiple possible routes.
That means that we can instead fill an array with promises for matching routes and then call Promise.all
on all matching routes to resolve the loadData
promise of each matching route.
Something a little bit like this:
import { matchRoutes } from 'react-router-config';
// ...
const matchingRoutes = matchRoutes(Routes, req.url);
let promises = [];
matchingRoutes.forEach(route => {
if (route.loadData) {
promises.push(route.loadData());
}
});
Promise.all(promises).then(dataArr => {
// render our app, do something with dataArr, send response
});
// ...
The renderRoutes
utility takes in our static route configuration object and returns the needed Route
components. renderRoutes
should be used in order for matchRoutes
to work properly.
So with renderRoutes
our App
component changes to this simpler version instead:
import React from 'react';
import { renderRoutes } from 'react-router-config';
import { Switch, NavLink } from 'react-router-dom';
import Routes from './routes';
export default props => {
return (
<div>
{/* ... */}
{renderRoutes(Routes)}
</div>
);
};
If you ever need a good reference for what we did here, have a look at the Server Rendering section of the React Router docs.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
came across the problem : “Invariant failed: You should not use <Switch> outside a <Router>”
Hey ,I am getting this error
css is not working
import “./App.css”;
What is the content supposed to be in for the posts and todos components?