Server Rendering, Code Splitting, and Lazy Loading with React Router v4
“Godspeed those who attempt the server-rendered, code-split apps.”
— Ryan Florence, Co-Creator of React Router
Some Background on Server Rendering at Airbnb
Taking this a step further, we introduced client side routing and route based code-splitting with React Router v3 as part of our architecture revamp. This is what enabled the smooth page transitions, and smaller initial page loads.
Enter React Router v4
Server rendering + code splitting boils down to a single requirement. In order for them to work together you need to be able to match against your current route before rendering.
(The rest of this post is fairly code heavy, if you aren’t already familiar with RRv4, take some time to read over the excellent React Router documentation. Also, it wouldn’t hurt to look over the webpack code-splitting documentation.)
The problem then, is that React Router v4 switched from a centralized route configuration (with a
getComponent function for async loading) to a decentralized version. Routes are now defined inline like so:
Defining routes in this manner means you won’t know which routes/components are needed to render your page until you are actually rendering. To demonstrate why this is a problem, imagine we’re async loading the
When we server render, we’ll want to render all of the content, so the html for the
About component will be generated and inserted into the DOM tree. On the client side though, we won’t know to match the
/about route nor would we know to load the
About component, until after we’ve already entered the render cycle. This will cause a client/server mismatch error since without having the
About component loaded, the client won’t create the same html as the server. This also likely means a flash of content and a wasted render, meaning a worse experience for your users.
To address the unique problems with inlining routes, they’ve created
react-router-config. This let’s you continue to define your routes in a centralized location, and match against them before triggering your initial render. Using the library, our routes definition might look something like this:
Decentralizing Our Re-Centralized Routes
(I know what you’re thinking…)
react-router-config is great, but there is still a bit of work left to do.
react-router-config doesn’t seem to have support for loading components in an async manner, and child routes are required to be too explicit. Notice that all the path values are defined as full paths.
This can get a little unwieldy, and limits reuse. To address this issue, we’ve implemented a mapping function that allows components to define child routes.
Doing this gives us a little bit more freedom during development. The grandchild route no longer needs to know its full path, and can be inserted into your central configuration at any location to create deeply nested routes.
This is extremely powerful, especially in large code bases. Routes can be reused in multiple locations, and component logic can be route agnostic, so their reusability increases as well. This is what makes our current transition into a large single page app (SPA) possible, as we can add additional routes without bloating our core flow. As our SPA grows over time to include more product pages, we can rest assured that any individual page only contains what is needed.
Defining an Async Route
We need to ensure that all the components, for whichever route a user hits, are loaded before we make the call to render.
To start, we’ll create an async component definition, then we’ll change our grandchild route to use the new component. Note the static load function, this will be used later to ensure that we’re ready to render.
Also, let’s update our grandchild route to leverage the new helper function.
Ensuring the Routes are Ready
Now we’re exporting a route config that contains a route with component that defines a static
load function. All that’s left to do before rendering is ensure that everything is loaded.
Putting it all Together
Now that everything’s in place, we’re finally ready to render our application. This is what everything looks like when tied together (minus the definitions for the helper functions
This post is a bit heavy with code, I thought it would be helpful to see all of this in action. Check out the demo repository to see everything put together, and feel free to explore the code!
We’re always looking for ways to improve, both in our core products and in the open source libraries that we depend upon.
- Opening issues/pull requests into
react-router-configin the hopes of making this process even simpler for others.
- Investigating how to minimize hydration size as more pages are pulled into our SPA.
**A Note on import()
import() you see in the above code is the relatively new dynamic import syntax (currently stage 3). This new syntax is meant to support async loading of ES modules. We’re taking advantage of it by using it to replace webpack’s
require.ensure calls in our source.
They can convert your large
require.ensure calls from this: