React + Redux + React Router 4 code splitting w/ RxJS & Webpack

Exploring code splitting techniques with Webpack & RxJS

Luigi Poole
7 min readMar 3, 2017

Reasoning

Speed. One of the most important parts of any website is it’s time to interactive.

By splitting our app into chunks we are able to load & evaluate only the code that is required for the page rendered, allowing us to retain short load times as the total pages in our app grows and become more feature rich.

Assumptions

The following post assumes the reader has previous experience with React/JSX, React Router & Webpack.

In the event you are not familiar with the libraries mentioned above I have included some helpful getting-started resources at the bottom of this post for each library respectively.

Tech

For this demo we will be using the following open source dependencies:

Completed example project w/ live demo for this post can be found at: https://github.com/luigiplr/react-router-redux-rxjs-code-splitting

Getting Started

Luckily for us Webpack already has code-splitting built in!

Clip from Андрей Вандакуров’s Sideshare presentation

We can use it by defining split breakpoints using the ES6import statement within our app like so:

const page1 = () => import('./page1.jsx')page1().then(loadedModule => {
console.log(loadedModule)
})

Note: If you’re using the ES6 module export syntax, you will need to modify the code above to use the loadedModule.default property of your newly loaded module. The code in the example project contains helper functions for this.

That's it. Webpack will now do the rest intelligently, any code directly referenced from within page1.jsx is now split into its own chunk.

An example of how to implement this logic into an React Router 4 Route is as follows:

React Router Route Definition using asyncRoute()

Above we have defined HomeRoute as a new asyncRoute() & passed the component we want to load as a function that returns our Module as a Webpack Chunk Promise.

Note: Webpack’s ability to split code into different chunks relies on static analysis of your apps code at build time, thus it is recommended to define your imports in-full prior to run-time & not use template strings for chunk importing.

While at first glance the creation of multiple asyncRoute()s would seem easily abstracted around a more parametric deceleration this would potentially have unintended side-effects (e.g. helper files being also split). An example of what happens when Webpack analyzes template literals vs static imports can be seen below.

/*
every "importable" file within "pages/" is split into its own
respective chunk. This can have adverse effects if our directory
contains helper files etc.
*/
const parametricRuntimeImport = name => import(`./pages/${name}`)
/* only intended files are split, no surprises. */
const staticImport = name => {
switch (name) {
case 'page1':
return import('./pages/page1')
case 'page2:
return import('./pages/page2')
}
}
AsyncRoute — Based off of Andrew Clark’s quick and dirty code splitting example

Here we curry the creation of the AsyncRoute Class behind asyncRoute() , This ensures every Route has its own AsyncRoute instance & allows us to pass our Chunk loader Promise creator as an argument.

As the component mounts it loads in our code-split chunk via our getComponent argument. Once loaded we set our static parameter (so if we return to this route we don’t try to re-load an existing chunk) & update our component state with our shiny new component.

Note: This Class should not be used for Server Side Rendering; How to implement Server Side Rendering in a code-spitted app will be covered in a follow-up post.

The Redux Conundrum

Redux makes managing our app state fun again.

Unsurprisingly for Redux to work all reducers must be loaded before they can consume any actions targeted at them. As we want to code split our reducers on a route-per-route basis we must create a reliable way to inject them; This is where our Redux Registry comes into play.

Redux store creation using our Registry

When using our Redux Registry our standard createStore() function must be modified accordingly.

Rather than combining our initial reducers we pass them as a object to the Registry constructor as baseReducers.

In our demo the initial reducers consists of a single dummy reducer with an empty object as its default state.

In addition to retrieving our initial combined reducers from the Registry we must assign the created store to our Registry, by assigning it to registry.store, so that we can interact with the Redux store’s api methods for Reducer replacements etc.

Redux Registry

Above we can see that our injectReducers() method merges the new reducers with our current reduces. While this method has no protection against double reducer injection, one could add it by omitting any duplicate keys in our new reducers that already exist in our current State. Injecting a reducer twice will result in Redux throwing an error.

Note: Our Registry is restricted to the injection of only new Reducers; The functionality could be expanded to include among other things injection of Redux Observable Epics & SSR Epics/Reducer re-hydration/bootstrapping.

Redux Registry middleware

Allowing us to “dispatch” reducer injections is our custom middleware that consumes any valid STORE_INJECT actions; We use a Symbol to ensure we never miss-interpret a non-injection dispatch.

Redux Registry actions

We use a custom action to pass messages to our Redux middleware; The use of ES6 computed property names makes this a breeze.

Sample Reducer that is being injected

A caveat to our Registry method is that every “injectable” reducer must have an reducer key assigned to its prototype with the desired name of the reducer within the Redux state.

Why RxJS

Think of RxJS as Lodash for events. — RxJS Introduction

RxJS provides us with an awesome way to create event driven cancelable Observables, it fits very nicely as page-navigation is also inherently event driven.

Its usefulness to us can be summed up in the following scenarios:

Scenario 1 (Not using RxJS)

Scenario 1 AsyncRoute component life-cycle methods
  • User lands on our site
  • User switches to Tab 1 & AsyncRoute begins loading in the Route Component & Reducer(s) respectively
  • User decides they would rather be on Tab 2 and quickly switches over
  • Tab 2’s AsyncRoute now begins to load all its Component & Reducer(s) respectively
  • Tab 1 finishes; begins to inject its reducers
  • Unfortunately for us our User is on a mobile device & we are now injecting two reducers while mounting a React component; Our site has become noticeably sluggish & we have kissed goodbye to any smoothness in our route transitions

Scenario 2 (Using RxJS)

Scenario 2 AsyncRoute component life-cycle methods
  • User lands on our site
  • User switches to Tab 1 & AsyncRoute begins loading in the Route Component & Reducer(s) respectively
  • User decides they would rather be on Tab 2 and quickly switches over, Tab 1’s componentWillUnmount() method is called and our Chunk loader subscriptions are torn down
  • Tab 2’s AsyncRoute begins to load all its Component & Reducer(s) respectively
  • As Tab 1’s reducers were never injected; Our app performs as expected with no noticeable slowdowns

Putting it all together

Now that we have the ability to load and inject reducer chunks we can modify our app.jsx so it resembles the following:

Upgraded app.jsx — now including code split reducers being passed to asyncRoute()

Much like before a Route’s component loader is passed as the first argument to asyncRoute(), We now have an optional second argument that takes an reducer loader.

Note: Should you require more Reducers per Route you can combine you’re import()s inside a Promise.all() as they are inherently native Promises.

Stats

Bundle before:

Bundle after:

Note: For best results use on bigger apps. 😛

Live demo: https://luigiplr.github.io/react-router-redux-rxjs-code-splitting

TL;DR;

  • RxJS makes user-induced chunk loading a breeze, problems that resulted in hacks to cancel Promises are no longer needed
  • React Router v4 is cool; Give it a try!
  • Its worth migrating to Webpack 2
  • Code split all the things

Feedback?

  • Did I screw up somewhere?
  • Have a better idea on how to do something?

I would love to hear/learn about it; Submit a Issue/PR!

Helpful Resources

Webpack:

React/JSX

React Router

RxJS

--

--