14 Dec 2017 Putting together the pieces: Server-side rendering with React Router v4 and Redux
Server-side rendering a React app is a miracle on-par with childbirth and modern air travel.
OK, that opening sentence was a little over-the-top. I apologise to birth mothers and those in the aviation industry.
Let me start again: server-side rendering a React app is…kind of cool.
That said, it can be a little tricky to get started, especially if you’re trying to do it with an existing app.
In this post I’ll explain one way you can implement server-side rendering (SSR) for an app that’s using React Router v4 and Redux Thunks.
Along the way we’ll discuss the fundamental difference between JavaScript clients and servers, how it forces us to change the way we do routing, and the small “missing-link” that enables us to bridge React Router v4 with Redux thunks.
We’ll build up a simple example to demonstrate. I’m going to assume you’ve got some knowledge of:
- React
- Redux
- React Router v4
However, you are not required to have knowledge of:
- Childbirth
- Aeronautics
Let’s do this.
Disclaimer
A warning: only do SSR if you have to. In other words, don’t do it just for fun.
Normally, we only do SSR for SEO (making it easier for Google to crawl your app) or performance (improving time to first paint) purposes. Without solid, measurable requirements, you shouldn’t do it. The tradeoff of doing SSR is that you have to start constraining the way you use React (and React Router v4), which is something you shouldn’t do unless you really have to.
Also, if you really think you’re going to need to do SSR, consider using a framework like next.js, or even a pre-renderer like Rendertron. Only if none of these solutions work for you should you try and do this stuff yourself.
Still sure you want to go ahead? Ok, here we go…
The Setup
Consider a (very) simple app that uses React, Redux and React Router v4 (which we’ll now refer to as ‘RRv4’). It supports two routes, /some-path
and /some-other-path
, which render the components SomeComponent
and SomeOtherComponent
respectively:
import ReactDOM from "react-dom"
import { BrowserRouter, Switch, Route } from "react-router-dom"
import { createStore, Provider } from "redux"
import { SomeComponent } from "./some-component"
import { SomeOtherComponent } from "./some-other-component"
const store = createStore(...) // Setup reducers and middleware
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<Switch>
<Route path="/some-path" component={SomeComponent}/>
<Route path="/some-other-path" component={SomeOtherComponent}/>
</Switch>
</BrowserRouter>
</Provider>,
document.getElementById("app")
)
SomeComponent
and SomeOtherComponent
are connected components. It doesn’t really matter what they render, but in order to do their job they need to start loading some data when they mount. For example,SomeComponent
does this using an action creator called getSomeData()
:
import React from "react" import { connect } from "react-redux" import { getSomeData } from "./actions" function mapStateToProps(state) { ... // Doesn't really matter what happens here } const mapDispatchToProps = { getSomeData } export const SomeComponent = connect(mapStateToProps, mapDispatchToProps)( class extends React.Component { componentDidMount() { this.props.getSomeData()) } render() { ... // Doesn't matter what happens here } } ) ...
The getSomeData()
action creator is asynchronous and uses thunks. It makes an asynchronous call that returns a promise, then when the promise resolves, dispatches an action to the store with the result:
... export function getSomeData() { return dispatch =&amp;amp;gt; { doSomethingAsync().then(result =&amp;amp;gt; { dispatch({type: "GOT_RESULT", result}) } } } ...
SSR v1
At first glance, changing this app to use server-side rendering seems straightforward. Looking at the related React and Redux docs, we could try writing a simple Express server that looks like this:
import express from "express" import http from "http" import ReactDOMServer from "react-dom/server" import { createStore, Provider } from "redux" import { StaticRouter, Route } from "react-router" const app = express() const server = http.createServer(app) app.use((req, res) =&amp;amp;gt; { const store = createStore(...) // Setup store with reducers, etc // Render the component hierarchy using the store, // include the routes const content = ReactDOMServer.renderToString( &amp;amp;lt;Provider store={store}&amp;amp;gt; &amp;amp;lt;StaticRouter location={req.url}&amp;amp;gt; &amp;amp;lt;Switch&amp;amp;gt; &amp;amp;lt;Route path="/some-path" component={SomeComponent}/&amp;amp;gt; &amp;amp;lt;Route path="/some-other-path" component={SomeOtherComponent}/&amp;amp;gt; &amp;amp;lt;/Switch&amp;amp;gt; &amp;amp;lt;/StaticRouter&amp;amp;gt; &amp;amp;lt;/Provider&amp;amp;gt; ) const serializedState = JSON.stringify(store.getState()) // Write the response back to the client res.send(` &amp;amp;lt;html&amp;amp;gt; &amp;amp;lt;body&amp;amp;gt; &amp;amp;lt;div id="app"&amp;amp;gt;${content}&amp;amp;lt;/div&amp;amp;gt; &amp;amp;lt;script&amp;amp;gt; window.__PRELOADED_STATE__ = ${serializedState} &amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp; &amp;amp;lt;/script&amp;amp;gt; &amp;amp;lt;!-- Can include client-side JS files links here --&amp;amp;gt; &amp;amp;lt;/body&amp;amp;gt; &amp;amp;lt;/html&amp;amp;gt; `) }) }) server.listen()
For each request, it creates a store, renders the component hierarchy, serialises the store, and sends it all back to the browser.
Note that we render the same Route
s, but this time inside a StaticRouter
. This will do a one-time match against the URL and only render the component for the route that matches.
Next, we tweak our client-side code to use the store that has been sent from the server, and to rehydrate our component hierarchy over the top of the markup that came from the server. I’ve highlighted the lines that have changed:
import ReactDOM from "react-dom" import { BrowserRouter, Switch, Route } from "react-router-dom" import { createStore, Provider } from "redux" import { SomeComponent } from "./some-component" import { SomeOtherComponent } from "./some-other-component" const store = createStore(..., window.__PRELOADED_STATE__) ReactDOM.hydrate( &amp;amp;lt;Provider store={store}&amp;amp;gt; &amp;amp;lt;BrowserRouter&amp;amp;gt; &amp;amp;lt;Switch&amp;amp;gt; &amp;amp;lt;Route path="/some-path" component={SomeComponent}/&amp;amp;gt; &amp;amp;lt;Route path="/some-other-path" component={SomeOtherComponent}/&amp;amp;gt; &amp;amp;lt;/Switch&amp;amp;gt; &amp;amp;lt;/BrowserRouter&amp;amp;gt; &amp;amp;lt;/Provider&amp;amp;gt;, document.getElementById("app") )
On the initial client-side render, the component hierarchy should exactly match that generated on the server-side, because the same state has been used to generate it. Also, because the BrowserRouter
has had the same set of routes rendered into it, it will match the same URL that the server did. Consequently, there should be no changes to the DOM.
After this point, the user can start interacting with the app and it should operate entirely within the browser.
The Problem
If you run this server and go to one of these routes, the markup the server returns to you won’t actually contain any loaded data. This is because componentDidMount
hooks don’t execute on the server. Components never actually mount there, because there is no DOM to mount them on.
And even if componentDidMount
did execute (or you did something dodgy like switching to using componentWillMount
instead), it still wouldn’t work. Why? Because the data loading is asynchronous, and the markup from the initial render will already be streaming back to the client by the time the asynchronous call returns. There is no second chance to render.
Fun fact: as a React programmer accustomed to programming exclusively for the client-side, when this realisation dawned on me, my world (briefly) collapsed. I was disorientated and confused, like a newborn baby flailing its arms around and slapping itself in the face. After a couple of days of that, followed by a 30-second review of the RRv4 docs that touch upon this topic, the realisation dawned on me: we have to load the data before we start rendering.
However, this requires us to make a significant change to the way we do our routing. Specifically, all of the data required by all of the components that will be rendered for a particular route needs to be loaded before any rendering happens. This means that, to support SSR, we need to associate data-loading logic with a single route rather than a component.
Making it happen
It is possible to structure our code so that we can associate data-loading with a route, but we need to constrain our usage of RRv4 a little. Specifically, we have to put any routes that load data into a static data structure, rather than sprinkling them through our code. That way, each server-side route can be explicitly associated with the data that it needs.
Funnily enough, there’s a RRv4 side-project called React Router Config that defines a data structure for describing static routes, and gives you convenient methods for rendering and matching against that data structure.
I recommend you look at the docs for React Router Config, but the long and the short of it is that it lets you define your static routes a little like this:
import { SomeComponent } from "./some-component" import { SomeOtherComponent } from "./some-other-component" export const routes = [ { path: "/some-path", component: SomeComponent }, { path: "/some-other-path", component: SomeOtherComponent } ]
and gives us a matchRoutes
function to find which elements in the data structure match against a particular URL:
import { routes } from "./routes" import { matchRoutes } from "react-router-config" const branch = matchRoutes(routes, "/some-path") // Returns: [routes[0]]
and a renderRoutes
function to render the data structure to a set of Route
components in a Switch
block:
import { routes } from "./routes" import { renderRoutes } from "react-router-config" const renderedRoutes = renderRoutes(routes) // Returns: // &amp;amp;lt;Switch&amp;amp;gt; // &amp;amp;lt;Route path="/some-path" component={SomeComponent}/&amp;amp;gt; // &amp;amp;lt;Route path="/some-other-path" component={SomeOtherComponent}/&amp;amp;gt; // &amp;amp;lt;/Switch&amp;amp;gt;
But what’s this got to do with data loading?
Good question. Well, there’s nothing to stop us from adding additional, arbitrary properties to our static routes data structure. For example, we could add to each route definition a function that loads data for that route:
import { SomeComponent } from "./some-component" import { SomeOtherComponent } from "./some-other-component" export const routes = [ { path: "/some-path", component: SomeComponent, loadData: // ? }, { path: "/some-other-path", component: SomeOtherComponent, loadData: // ? } ]
But what exactly do these loadData
functions do, how do they get invoked, and how do their results get merged into the app? This is where the existing documentation gets kind of hazy. The RRv4 docs state that “(t)here are so many different approaches to this, and there’s no clear best practice yet”. Which is all well and good, but it’d be nice to at least know one way to do it.
The good news is that because we’re using thunks, there’s an easy answer: we can use action creators as our loadData functions. But for this to work, we’ll have to make one small change:
... export function getSomeData() { return dispatch =&amp;amp;gt; { return doSomethingAsync().then(result =&amp;amp;gt; { dispatch({type: "GOT_RESULT", result}) } } } ...
Can you see the difference? We’re now actually returning from the thunk the last promise in the promise chain. This is very important, because that promise will also be returned by dispatch()
when we call it with getSomeData()
. In other words, if we do this:
const promise = store.dispatch(getSomeData())
then not only will some async stuff happen and the GOT_RESULT
action be dispatched to the store, but we’ll also get the promise that was returned by the thunk. This means we can know when then the asynchronous action creator will have finished doing its work. This is the missing link that we need to bridge between React Router and Redux.
Returning to our static routes data structure, we can pop in our asynchronous action creators as the loadData
function for each particular route:
import { SomeComponent } from "./some-component" import { SomeOtherComponent } from "./some-other-component" import { getSomeData, getSomeOtherData } from "./actions" export const routes = [ { path: "/some-path", component: SomeComponent, loadData: () =&amp;amp;gt; getSomeData() }, { path: "/some-other-path", component: SomeOtherComponent, loadData: () =&amp;amp;gt; getSomeOtherData() } ]
and be confident we’ll have a way to know when the data for that route has finished loading. Which means we now know when it’s safe to start rendering HTML for that route.
Putting it together
Let’s start putting this together by extending our Express server. Specifically, for each request, we’ll dispatch to the store the loadData
action for any routes that matches the request and wait for them all to finish, before we render the response:
import express from "express" import http from "http" import ReactDOMServer from "react-dom/server" import { createStore, Provider } from "redux" import { StaticRouter } from "react-router" import { renderRoutes, matchRoutes } from "react-router-config" import { routes } from "./routes" const app = express() const server = http.createServer(app) app.use((req, res) =&amp;amp;gt; { const store = createStore(...) // Setup store with reducers, etc const { url } = req // For each route that matches const promises = matchRoutes(routes, url).map(({route, match}) =&amp;amp;gt; { // Load the data for that route. Include match information // so route parameters can be passed through. return store.dispatch(route.loadData(match)) }) // Wait for all the data to load Promise.all(promises).then(() =&amp;amp;gt; { // Now render the component hierarchy using the store, // include the routes const content = ReactDOMServer.renderToString( &amp;amp;lt;Provider store={store}&amp;amp;gt; &amp;amp;lt;StaticRouter location={url}&amp;amp;gt; { renderRoutes(routes) } &amp;amp;lt;/StaticRouter&amp;amp;gt; &amp;amp;lt;/Provider&amp;amp;gt; ) const serializedState = JSON.stringify(store.getState()) // Write the response back to the client res.send(` &amp;amp;lt;html&amp;amp;gt; &amp;amp;lt;body&amp;amp;gt; &amp;amp;lt;div id="app"&amp;amp;gt;${content}&amp;amp;lt;/div&amp;amp;gt; &amp;amp;lt;script&amp;amp;gt; window.__PRELOADED_STATE__ = ${serializedState} &amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp;&amp;amp;nbsp; &amp;amp;lt;/script&amp;amp;gt; &amp;amp;lt;!-- Can include client-side JS files links here --&amp;amp;gt; &amp;amp;lt;/body&amp;amp;gt; &amp;amp;lt;/html&amp;amp;gt; `) }) }) server.listen()
This server assumes that each static route actually has a loadData
function. It also passes any additional information about the route to the loadData
function, which can sometimes be useful when the route contains parameters.
The fantastic thing about this approach is that our action creators can be arbitrarily complex. For example, they could dispatch multiple asynchronous calls, either serially in parallel. As long as each thunk returns a single promise that resolves when all of the asynchronous work is done, then we can be confident that, when all of the promises have resolved, the store will be in the correct state and our component tree can be rendered correctly.
On the client-side, we can now reuse our routes, this time in the context of the BrowserRouter
:
import ReactDOM from "react-dom" import { BrowserRouter } from "react-router-dom" import { createStore, Provider } from "redux" import { renderRoutes } from "react-router-config" import { routes } from "./routes" const store = createStore(..., window.__PRELOADED_STATE__) ReactDOM.hydrate( &amp;amp;lt;Provider store={store}&amp;amp;gt; &amp;amp;lt;BrowserRouter&amp;amp;gt; { renderRoutes(routes) } &amp;amp;lt;/BrowserRouter&amp;amp;gt; &amp;amp;lt;/Provider&amp;amp;gt;, document.getElementById("app") )
The added bonus is that by reusing the same data structure on both the client and the server, we can be doubly confident that they’ll be supported correctly in both environments.
One last thing
What we’ve done so far looks great, but there’s one final thing we need to take care of.
Like any good single-page app, after we’ve loaded the app once from the server-side, any subsequent route changes should happen purely inside the browser, without any further round-trip calls to the server. Using the RRv4 Link
component or history
API to trigger route changes will ensure this.
However, even though we can match the same set of routes on the client-side, there’s nothing in the client-side code that will actually trigger the loadData
functions for those routes. Consequently, whilst the new route might get displayed on the client-side, no data will be loaded for it.
To remedy this, we can write a component exclusively for the client-side whose sole job is to listen for browser location changes, look up the routes that match the location, and trigger the loadData
action for those routes. Here’s what it looks like:
import React from "react" import { withRouter } from "react-router-dom" import { matchRoutes } from "react-router-config" export const RouteDataLoader = withRouter(class extends React.Component { componentWillReceiveProps(nextProps) { if (nextProps.location != this.props.location) { matchRoutes(this.props.routes, nextProps.location).forEach(({route, match}) =&amp;amp;gt; { this.props.dispatch(route.loadData(match)) }) } } render() { return this.props.children } })
This component uses React Router’s withRouter
higher-order component wrapper to automatically get access to the browser location as a prop. It also accepts dispatch
and routes
props. Note that, unlike our server, it ignores the promises returned by loadData()
, as any changes to the client-side store will automatically trigger a re-rendering in the browser.
We can now update our client code to use RouteDataLoader
:
import ReactDOM from "react-dom" import { BrowserRouter } from "react-router-dom" import { createStore, Provider } from "redux" import { renderRoutes } from "react-router-config" import { routes } from "./routes" import { RouteDataLoader } from "./route-data-loader" const store = createStore(..., window.__PRELOADED_STATE__) ReactDOM.hydrate( &amp;amp;lt;Provider store={store}&amp;amp;gt; &amp;amp;lt;BrowserRouter&amp;amp;gt; &amp;amp;lt;RouteDataLoader routes={routes} dispatch={store.dispatch}&amp;amp;gt; { renderRoutes(routes) } &amp;amp;lt;/RouteDataLoader&amp;amp;gt; &amp;amp;lt;/BrowserRouter&amp;amp;gt; &amp;amp;lt;/Provider&amp;amp;gt;, document.getElementById("app") )
Now our app will automatically load data for new routes when the browser location changes. Note that because RouteDataLoader
uses componentWillReceiveProps
, it won’t actually trigger any data loads when it first mounts. This is exactly what we want, as the initial render will be laid over the markup we’ve received from the server-side, which will have already loaded the data.
Wrapping Up
In this post I’ve presented one way in which you can piece together an SSR solution using React Router v4 and thunks. By leveraging the React Router Config project, we were able to extract our routes into a separate data structure that could also have data-loading logic attached to it. We were then able to exploit thunks to know when any asynchronous action creators had finished.
Along the way we’ve seen how strategies for routing and data loading on the server-side must fundamentally differ from those you would use in a client-side-only app. This is why I only recommend you do SSR if you absolutely have to. The results can be miraculous (although admittedly not a match for childbirth or manned flight), but you’ll still have to be prepared to seriously constrain the way you structure routes and load data in your app.
hehe (@xu33)
Posted at 18:47h, 22 FebruaryWhat to do if I only want to to ssr on the Index Route,but do other route on client?
Because only the index page has the direct Entrance place(from a native app)
How could i write static route on server?
pranayyelugampranay
Posted at 19:35h, 27 FebruaryCould you tell me how to integrate this in the create react app ?