Code splitting Redux reducers

Adam Rackis
Frontend Weekly
Published in
6 min readSep 5, 2017

Redux, while incredibly popular, can be difficult at times to get the hang of. It provides low-level primitives, leaving you to put them to good use architecting your application. While concerns like data normalization, selector memoization, etc have been discussed at length elsewhere, how Redux fits in with code splitting has, as near as I can tell, been covered less thoroughly.

This post aims to discuss how Redux allows developers to lazily load pieces of their application as needed, rather than all at once, up front. Specifically, let’s assume the goal is to build an application where, as different modules are routed to, not only the needed components, but also reducers are loaded on demand, and integrated seamlessly.

Before we start, I’ll note that I’m assuming the reader is familiar with the basics of Redux: Middleware, combineReducers, etc.

The most basic Reducer code splitting, possible

To establish a baseline let’s start with the simplest reducer imaginable — the canonical counter.

Here we’re setting up a store with our core reducer which has a few properties, which can be incremented or decremented. We’re then exposing the result of this reducer under the “app” key. In real life this reducer may manage state central to the application, like authentication info, application-wide notifications, etc.

But in real life we also want modules to be able to introduce their own reducers, dynamically. Let’s look at how to do that, next.

store.replaceReducer

The store already has a method which allows us to swap in a new reducer, to which we can pass an object with new keys, representing the reducers that have been dynamically loaded. I’ll first brute force it, and then see about abstracting to something more generally useful. Note that I’m keeping these dynamically loaded reducers as simple and trivial as possible, to not distract from the central concepts being discussed.

So we start with the same basic reducer as before, and then one by one we add in new reducers using store.replaceReducer. Obviously each successive run requires that we manually also bring along each previously-added reducer. Naturally this is not feasible for any real application; however, since all we’re working with are objects and properties, this can easily be simplified with object spread.

Let’s instead keep a running list of all code-split reducers, and use a central method, getNewReducer, to add to this list, which then calls replaceReducer appropriately.

Same as before, except now we have a central method, getNewReducer, which will do the plumbing of adding in our code-split reducers.

The above code is based on this gist by Dan Abramov: https://gist.github.com/gaearon/0a2213881b5d53973514

The code is great, but it has one fatal flaw…

Improving our setup for saved state

The gist above will not work with pre-loaded, saved state. createStore actually has a second argument, for initial state. This is useful if you’re serializing your redux state to localStorage (or elsewhere), and then trying to hydrate your redux state when the user comes back to your web app. For example, consider this code

This code attempts to pre-load our reducer with some saved state from our core app reducer, as well as the lazily-loaded aModule, and bModule reducers. Unfortunately, it won’t work. Initially we’ll get the warning in our console of

Unexpected keys “aModule”, “bModule” found in preloadedState argument passed to createStore. Expected to find one of the known reducer keys instead: “app”. Unexpected keys will be ignored.

and worse, as soon as we dispatch our first action, the aModule and bModule pieces of our saved state will disappear, which makes sense since we have no reducers present to carry them along; they’re not loaded yet.

Supporting saved state for code-split reducers

Let’s imagine we have acombineLazyReducers function, used in place of combineReducers, like this

Let’s assume combineLazyReducers was written by us, for the sole purpose of creating a reducer that doesn’t lose saved initial state. Of course we’ll need to pass it the initial state we wish to preserve, which we’ll do both with our intial state loaded from local storage, which we also pass to createStore, and also from getNewReducer, from which we’ll pass the current state in the store.

Let’s go through some iterations to see what combineLazyReducers might look like.

Keeping Saved State: Attempt 1

The first attempt comes from Redux co-creator Dan Abramov. We just use combineReducers as normal, but then wrap that call with object spread, like so

This slickly passes back the initial state every time, but allows if to be overridden by matching keys in the reducer created by combineReducers. Unfortunately there’s a problem: we still get annoying warnings in the console, like so

Unexpected keys “aModule”, “bModule” found in preloadedState argument passed to createStore. Expected to find one of the known reducer keys instead: “app”. Unexpected keys will be ignored.

If you don’t care about these warnings, or if Redux ever removes this warning, which I’ve heard talk of, then this solution would be fine. But let’s see about solving this in a way that obviates the warnings altogether.

Keeping Saved State: Attempt 2

Our problem is, there are pieces of state which lack corresponding reducers, since they haven’t loaded yet. Can we just detect which reducers are missing, and throw in stubs? Absolutely. Let’s take a look.

So we go through each key in our initialState, and if there’s not a corresponding key in the reducers object, we add one. The reducer stub we’re adding

state => state === undefined ? null : state

is a little funky, so let’s break it down. If the state passed in is undefined, we return null, else we just pass along the existing state. Why? Why not just do

state => state

to just always pass along the current State? Well let’s try it:

Uncaught Error: Reducer “aModule” returned undefined during initialization. If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined.

The error says it all. Redux will initially pass undefined for the state argument, and gibberish for the action, and this is your cue to return your default state. That’s exactly why the idiom for Redux reducers is for the state argument to have a default value, set to your initial state (default parameter values are triggered when undefined is passed) and to always return the passed in state if no action matches.

A needlessly complex solution from the future

The above solution is probably what I’d recommend for any real-world code. It’s simple, direct, clean, and it works. But just for fun, let’s explore an alternative: Proxy.

Proxies, essentially, allow you to intercept calls to an object. You can intercept property access, method calls, etc. What if we pass a Proxy to combineReducers which is configured to produce the stub above, if no corresponding reducer key is present? Let’s try:

A full discussion of Proxy is beyond the scope of this post; any ES6 book out there will likely discuss it at length. For our purposes, briefly, we’re telling our Proxy that we wish to override the default behavior of ownKeys, called when we say Object.keys on an object; get, which is for property access; and getOwnPropertyDescriptor. We override ownKeys to return not only all keys in our reducer object, but also all keys in our initial state, while wrapping both collections in a Set to prevent duplication. The get trap returns the property in the “real” reducer, if it exists, or the stub we saw before. Finally, by spec, anything returned by own keys must also have a property descriptor defined, so we trap that as well, and return the descriptor from the initial state, if needed.

It’s worth stressing that this last Proxy solution will not work on any old browsers, and cannot be worked around: it is impossible to polyfill a Proxy. So this solution can only ever work if you’re only supporting modern browsers, and even then it’s questionable whether this adds any value over the antecedent solution we saw.

Conclusion

Redux is a wonderful utility for web development. It can be difficult at times to learn, but I’ve always found it to be incredibly flexible and pliable. Hopefully this post had some takeaways to help developers code split their applications more aggressively, helping to keep initial js payloads small and fast.

Hit me up on Twitter at @adamrackis if I missed anything.

Happy Coding

--

--