Redux with Code-Splitting and Type Checking
Before We Get Started
This article assumes a working knowledge of Redux, React, React-Redux, TypeScript, and uses a little bit of Lodash for convenience. If you’re not familiar with those subjects, you might need to do some Googling. You can find the final version of all the code here. Also, follow me on Twitter @MatthewGerstman.
Redux has become the go-to state management system for React applications. While plenty of material exists about Redux best practices in Single Page Applications (SPAs), there isn’t a lot of material on putting together a store for a large, monolithic application.
Before we dive into code, let’s outline the architecture we’re about to build.
We need to create the store in such a way that we can register reducers asynchronously. This allows us to async load code associated with those reducers.
We need to type the store in such a way that it knows about all possible reducers we can register. This allows us to ensure static typing of all components at runtime.
Creating the Store
In order to code split, we need to instantiate the store in a way that allows us to register reducers after store creation. We start with the following code:
What’s going on here? We’re instantiating the store with the file and we’ve also exported two functions. One called
getStore, which is simply a wrapper around the store and doesn’t need much further explanation, and
registerReducer is the more interesting function. We maintain a map of existing reducers internal to the module and then replace add new ones as they come in. We then call
replaceReducer on the store and replace it wholesale.
replaceReducer is smart enough to maintain the state of the reducers that were previously there and fires an
INIT action for the new ones to populate their default state.
This is what makes code-splitting possible. We don’t care when the reducer is registered and all of that code can be loaded after the store is created.
Now let’s dig into what makes this type safe. Well, let’s dig into our
You’ll notice we import
WizardNamespaceShapefrom elsewhere in the codebase. This is okay. Because these are type-only imports, most build systems won’t actually bundle them in when building packages. This is where the statically typed code splitting magic happens.
We then export types
ReducerMap, which allow us to register all possible types on the actual state object in advance. Because we colocate the namespace keys in the types file, our developers can ensure that there are no key conflicts.
You’ll notice these types are both
Partial, so how do we enforce that a reducer is actually registered? Well, we do that in the selector layer.
Our selector layer is what ensures that we always have the reducers registered that we need. We can do this with a simple helper function.
getStateAtNamespaceKey complains very loudly if you attempt to access a namespace that hasn’t been registered yet. This is the only way we should access our data. As long as you call
registerReducer in the same part of your tree as your
<Provider /> component, your namespace should be registered by the time you get down to a
connect. We’ll elaborate on this in a moment.
Writing Actual Product Code
This is is all well and good, but let’s talk about what our product code looks like.
The code above is (hopefully) straightforward. We connect to the Redux store using
react-reduxand use the
connectcomponent/HOC respectively. We take a list of wizards and render them out to the screen, along with information about what spells they know and the status of their parents. Spoiler: We’re getting to a certain boy wizard with a lightning scar.
The two novel bits of code here are
getWizards. Let’s dig into them both.
In the above code, you saw what we call the
getStorefunctions that we declared before. We pass
registerReducera map with the key for the Wizard namespace and the Wizard reducer. Another important note: if we try to pass the wrong key or even the wrong reducer to
registerReducer, type checking will complain about it.
One last but crucial bit. We wrap
lodash.once. This ensures that we only register the reducer once and then always return the same instance of the store. While this isn’t strictlyrequired,
replaceReduceris an expensive noop, if called repeatedly.
This one is much more straightforward. We call
getStateAtNamespaceKeyand spit out the wizards to the user.
Sweet! We’ve set up the store, registered our reducer, and even built some components. Now let’s talk about how we can strongly type our actions. We do this in both the action layer and the reducer layer.
You’ll notice that we have two action types:
KillParentsAction. These actions each have a strongly-typed payload and their type is a predetermined string enum. We also export
WizardAction, which is useful in our reducer.
This is one of those occasions where TypeScript is truly brilliant. Our given action type is any of the
WizardActionTypes. Because each of them has their own defined
typeproperty, our switch statement will actually strongly type
action.payloadafter we determine its type. If we were to put any invalid code here, TypeScript would complain.
The last question to answer here is: “How do we get initial data into the store?” That’s done through a process called store hydration. What this means is that we’re going to dispatch an action that sets the state. Let’s take a look at this code.
First, we update our
actions.tsfile as shown.
Second, we add another switch statement to our reducer.
Third, we need to make an action creator.
Finally, we dispatch the hydration action from our store creation function.
lodash.onceis now extra useful because we will only ever populate the store once.
I hope this article helped you get started with Redux. At compile time, our store is strongly typed and has knowledge of the entire system. At runtime, we can code split however we’d like.
Glossary of Functions
This is a list of the core functions and types and the roles they serve in this architecture.
- NamespaceKey — A key for a reducer or namespace within the state object.
- ReducerMap — Object of all possible keys we can have on our store and their matching reducers. Is declared as a partial because it is not guaranteed that any given namespace is on the store.
- StoreShape — Object of all possible keys we can have on our store and their state shapes. Is declared as a partial because it is not guaranteed that any given namespace is on the store.
- hydrateWizardNamespace — Product layer function that provides initial state for the wizard namespace after the reducer is registered.
- getStoreForWizardApp — Product layer function that registers the “wizard” namespace within the store.
- getStateAtNamespaceKey — Function that grabs a namespace from the state object and fails quickly if that namespace is unregistered. Used to make our selectors type safe.
- registerReducer — Function that injects a reducer into the store after page load. Ensures that we only register known reducers at the typing layer.
Publishing an article like this takes a village. I want thank a whole bunch of people for their contributions.
@acemarke — For maintaining Redux and inspiring me to write this article.
@brianlink — Typo patrol.
@donavon — For adding and removing commas like a boss.
@goingglacial — For a thorough code review before merging the source this is based on.
@hswolff — For teaching me Redux in the first place.
@jetpacmonkey — For finding a bug in this article.
@peterpme — For code reviewing the article.
@swyx — For providing technical feedback on this article.
Andrew H — For article feedback
Justin K — For copy editing and fixing my atrocious grammar.
Matt S — For relevant life advice.
Yoeun P — For pair programming with me when I wrote the source this is based on.