React + Redux + React Router 4 code splitting w/ RxJS & Webpack
Exploring code splitting techniques with Webpack & RxJS
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:
- Webpack 2 (While it is possible to use Webpack 1.x it is recommended that you migrate)
- React Router 4 beta-6
- RxJS 5
- React 15.x
- Babel 6 w/ an assorted plugins & presets
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!
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:
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')
}
}
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.
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.
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.
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.
We use a custom action to pass messages to our Redux middleware; The use of ES6 computed property names makes this a breeze.
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)
- 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)
- 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:
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:
- Getting started with Webpack 2
- Webpack Official Site
- Tutorial to setup Webpack 2 from scratch (Github)
React/JSX
- Hello World (React offical docs)
- Getting Started with React and JSX