Making CRA apps work with SSR — Part 3

Routing with react-router

This is the third post in my CRA apps with SSR series. In the previous parts, I have added the basic SSR, asset imports, and redux to the mix.

This time, we’ll work on routing our app. We’ll look at two types of routing with react-router — static routes and dynamic routes. There’s also route params and redux integration.


To start off, clone the project repo, checkout tag part-3-start, install the dependencies and check if everything works.

git clone https://github.com/zhirzh/cra_with_ssr my-app
cd my-app
git checkout part-3-start
cd client/
npm i
npm run build
cd ../server/
npm i
npm start
# open http://localhost:3000/ in browser

Adding react-router

Client side

Add the react-router-dom package and extract the content in <App /> into a new <Home /> component. We’ll use <App /> for routing.

The first route is for loading <Home /> and the second is a catch-all route.

# client/
npm i react-router-dom
// App.jsx
import React, { Component } from 'react';
import { Route, Switch } from 'react-router-dom';
import Home from '../Home';
function NoMatch() {
return (
<div>
<h1>404</h1>
Page Not Found
</div>
);
}
class App extends Component {
render() {
return (
<div>
<Switch>
<Route path="/" component={Home} exact />
<Route render={NoMatch} />
</Switch>
</div>
);
}
}
export default App;

Once you copy the <Home /> component from github, you can start the dev server to see if everything works.

# client/
npm start
Route “/” and catch-all in action

Server side

Now that we have tested our routing logic on the client side, we can set it up on the server side.

# server/
npm i react-router

The official React Training SSR guide gives us some tips on how to add routing to the server side. First off, we’ll use react-renderer.js as an express middleware rather than a simple route as we did up until now.

// app.js
--- app.get(['/', '/index.html'], reactRenderer);
+++ app.get('*', reactRenderer);

In react-renderer, we need to match the request URL to a list of URLs that the client can handle. If we get a match, render the react app. If not, just call the next() middleware in the chain.

// react-renderer.js
const { matchPath, StaticRouter } = require('react-router');
const routes = ['/'];
function reactRenderer(req, res, next) {
const match = routes.find(route =>
matchPath(req.path, {
path: route,
exact: true,
}),
);
// bail
if (!match) {
return next();
}
// ...
const context = {};
const myApp = renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
</Provider>,
);
// ...
}

And with just that, we have our minimal routing config.


Route params

For trying out route params, we’ll use a different component. Grab the code for <Page1 /> from github.

The react-router website doesn’t say much about route params. Here’s a list of possible patterns:

  • Named: /path-1/:foo
  • Named-glob: /path-1/:foo*
  • Named-one-glob: /path-1/:foo+
  • Named-optional: /path-1/:foo?
  • Named-regex: /path-1/:foo(\d+)
  • Unnamed: /path-1/(.*)
  • Nested: /path-1/:foo/bar
  • Glob: /path-1/*

Note: When mixing different different patterns, it is really important to account for their selectivity. For example, Glob will match anything that follows /path-1/*, whereas Named-Regex will only ever match the given the regex.

Note: All patterns used in <App /> must also be listed in react-renderer and in the same order, as shown below.

// client/src/App.jsx
<Route path="/page-1/:numbers(\d+)" component={Page1} />
<Route path="/page-1/:alphabets([a-zA-Z]+)" component={Page1} />
<Route path="/page-1/:any" component={Page1} />
<Route path="/page-1/:any_regex(.*)" component={Page1} />
<Route path="/page-1/(.*)" component={Page1} />
<Route path="/page-1/*" component={Page1} />
<Route path="/page-1/:any_optional?" component={Page1} />
<Route path="/page-1" component={Page1} />
// server/src/react-renderer.js
const routes = [
// ...
  '/page-1/:numbers(\\d+)',
'/page-1/:alphabets([a-zA-Z]+)',
'/page-1/:any',
'/page-1/:any_regex(.*)',
'/page-1/(.*)',
'/page-1/*',
'/page-1/:any_optional?',
'/page-1',
];

Connecting Redux

There isn’t anything new to do — create reducer, pass the new reducer to the redux store, connect react component to the store and add new state slice on the server.

Please refer to this commit to see the changes in code.


Asynchronous Routing

Thus far, everything was done in a synchronous fashion. But loading everything synchronously can take a long time. We can speed things up by loading things as and when needed. This where code splitting comes in.

The official React Training code-splitting guide provides a super easy way to go about it. It describes a <Bundle /> component that handles the async flow. We pass it a function that calls dynamic import() to load a module.

However, the coming version of react-router-dom has a new approach to code-splitting using react-loadable.

We will look at both approaches — async component and react-loadable.


Async Component on Client

I tweaked <Bundle /> a little to use promise chaining instead of callbacks. And I renamed it to <Async />.

Add the <Async /> component to clients/src/components/. You can grab the code from github.

We’ll also create a new page component <Page2 /> to load asynchronously. Here’s the code.

Next, we will update the routes in <App />. The function renderPage2() imports the module Page2.jsx when the user navigates to /page-2 route.

// App.jsx
import Async from '../Async';
import Home from '../Home';
import Page1 from '../Page1';
// ...
function renderPage2(props) {
return (
<Async load={
() => import(/* webpackChunkName: "Page2" */ '../Page2')
}>
{Page2 => <Page2 {...props} />}
</Async>
);
}
class App extends Component {
render() {
return (
<div>
<Switch>
// ...
<Route path="/page-2" render={renderPage2} />
<Route render={NoMatch} />
</Switch>
</div>
);
}
}
Request to chunk for <Page2 />

Open http://localhost:3000/page-2 to see a new request to Page2.chunk.js. You can change the name for the chunk in webpack’s import().

This works well on the client side. But server rendering async routes is a little tricky. If we simply add the route to react-renderer.js, it won’t render <Page2 /> on the server.

// server/src/react-renderer.js
const routes = [
'/index.html',
'/',
  ...
  '/page-2',
];
<Page2 /> not rendered on the server

Async Component on Server

If you look at the server log, you’ll see a new message:

Warning: setState(...): Can only update a mounting component. This usually means you called setState() outside componentWillMount() on the server. This is a no-op.

React is telling us that we can’t update the state after loading the component. We need to load all our modules synchronously on the server, without messing up our client setup.

We do this by adding a new environm variable in client/config/env.js that looks for process.env.SERVER and passes it to React.

// client/config/env.js
const raw = Object.keys(process.env)
.filter(...)
.reduce(...);
raw.SERVER = 'SERVER' in process.env;
// Stringify all values so we can feed into Webpack DefinePlugin
const stringified = ...;

We now set the SERVER env var in server/src/app.js.

// server/src/app.js
const { BUILD_DIR, PUBLIC_DIR } = require('./paths');
process.env.SERVER = true;
require('./browser-polyfill');

Build the app and start the server.

# client/
npm run build

# server/
npm start
<Page2 /> correctly rendered on the server

react-loadable

Custom components are totally fine. But as I pointed out, react-router-dom will soon be shifting to react-loadable for code-splitting. I’d say this is an opportunity to pick up something new and stay ahead of the curve.

To start off, add RL to both, the client and the server project.

# client/
npm i react-loadable
# server/
npm i react-loadable

RL on client

A minimal RL setup is… pretty minimal.

Calling the Loadable() method returns a react HOC that dynamically loads a module before rendering it. We pass it 2 required props:

  1. loader: A function returning a promise that loads your module.
  2. loading: A react component that is rendered while the module is loading or when it errors.
import Loadable from 'react-loadable';
const AsyncFoo = Loadable({
loader: () => import('./components/Foo'),
loading: () => <div>Loading...</div>,
});
class App extends Component {
render() {
return <AsyncFoo />;
}
}

We follow similar steps in our react app.

// App.jsx
import Loadable from 'react-loadable';
...
const AsyncPage2 = Loadable({
loader: () => import(/* webpackChunkName: "Page2" */ '../Page2'),
loading: () => <div>Loading...</div>,
});
class App extends Component {
render() {
return (
<div>
<Switch>
<Route path="/" component={Home} exact />
          ...
          <Route path="/page-2" component={AsyncPage2} />
          <Route render={NoMatch} />
</Switch>
</div>
);
}
}

RL on server

The basic requirement for server rendering async routes is the same — load all components before mounting the root component. In the previous section, we used synchronous loading. RL uses promise chaining.

RL provides a method preloadAll() that loads the dynamic modules and returns a promise. We need to defer starting our server until this promise resolves.

// server/bin/www
const app = require('../lib/app');
const debug = require('debug')('my-app--server:server');
const http = require('http');
const Loadable = require('react-loadable');
...
Loadable.preloadAll().then(() => server.listen(port));
server.on('error', onError);
server.on('listening', onListening);

We can now test our setup.

# client/
npm run build
> my-app--client@0.1.0 build /my-app/client
> npm run lib && node scripts/build.js
> my-app--client@0.1.0 lib /my-app/client
> rm -rf lib/ && node scripts/lib.js
src/components/Home/Home.css -> lib/components/Home/Home.css
...
src/registerServiceWorker.js -> lib/registerServiceWorker.js
Creating an optimized production build...
Compiled successfully.
...
# server/
npm start
> my-app--server@0.0.0 start /my-app/server
> npm run lib && node ./bin/www
> my-app--server@0.0.0 lib /my-app/server
> rm -rf lib/ && babel src/ -d lib/
src/app.js -> lib/app.js
...
src/routes/users.js -> lib/routes/users.js

Open http://localhost:3000/page-2 in browser and check the page source for server renderer <h1>Page2</h1>.


BUT WAIT!

We can further optimise our CRA+SSR dynamic-routing setup, but it’s not that easy. Even the folks at ReactTraining find it tough. Luckily, the RL guide has a separate section for just this. But I’ll save it for a different time.


The End

This is end of the Part 3 of my CRA apps with SSR series. We added routing with react-router, connected redux, handled route params and even added dynamic routes via code-splitting on both — the client and the server.

That’s a lot for a single post. This may become the last part, since I have run out of ideas on what to do next. If you’ve got some, please share ’em with me.

The code for different parts is on github:

Godspeed who attempt server-rendered, code-split apps.