Some time ago, I wrote several posts about encapsulating the Redux state tree using reducers and selectors, the asymmetry between reducers and selectors, and the problems that arise when attempting to resolve that asymmetry in a modular-structured Redux application. Thanks to a wonderful blog post by Nicholas Gallagher, I now have a better solution to the problems.
As a recap, here are my original posts:
Where Did We Leave Off?
To summarize the earlier posts very briefly, we’ve defined selectors within our Redux modules that work on the module’s local section of the state tree. We use these selectors in each module’s reducer and its tests.
We also need versions of these selectors that work on the global state tree. We’ll use these in containers and thunk action creators both within the module, and potentially in other modules.
In order to get from a local selector to a global selector, we need to know how to get from the root of the state tree to the module’s local section of the tree. The main app reducer knows how to do that. So the main part of the app could be the one to apply that knowledge to create globalized selectors. However, since we need access to those globalized selectors within the module, we end up with a circular dependency:
In Modular Reducers and Selectors, we talked about the “textbook” ways of solving the cyclic dependency problem:
Apply the Dependency Inversion Principle by extracting something within the module that both the app and the rest of the module depend on; or
Create a new module that both the app and the module depend on.
We looked at Jack Hsu’s approach, which is to define a constant within the module. This constant is used to globalize the selectors and is also used by the main app reducer to specify where in the state tree the module’s reducer is mounted. This a form of the Dependency Inversion Principle approach.
We also looked at living with a bit of duplication. The app reducer specifies where to mount the module’s reducer in the state tree, and we duplicate that knowledge within the module in order to globalize the selectors. Up until now, that’s been my approach, though I still like Jack’s idea as well.
In all of this discussion about modular Redux applications, I didn’t understand one of the biggest advantages of a modular Redux architecture: code-splitting. In a large application, you often want to load only the parts of your code that are needed for a particular section of the application. A good modular structure can make it much easier to define the boundaries of the split.
But how do you code-split a Redux application when there is a single store?
This is where Nicholas Gallagher comes in. His post, Redux modules and code-splitting, tackles this question and comes up with a really nice approach involving a reducer registry.
As a side-effect, this reducer registry is an elegant solution to the circular dependency problem. It takes Jack’s idea of extracting a constant within the module and combines it with the “create a new module that both the app and the module depend on” approach.
What Does It Look Like?
I won’t repeat all of Nicholas’ code here, but the basic idea is that there is no more top-level app reducer that pulls in all of the module reducers. Instead, there is a
ReducerRegistry in a separate module.
In the main part of the application, we create the Redux store by combining all of the reducers that the
ReducerRegistry knows about.
Each module in turn registers itself with the
ReducerRegistry using its own
moduleName constant. As a side benefit, this same
moduleName constant can be used in other places, such as when creating action type constants for the module.
The dependency structure ends up looking like this:
I tested this approach in a sample application to see what it would look like. It worked great and I’m happy with how it turned out.
I did run into one little gotcha that’s worth noting.
Before, I’d export my module reducer from the module’s
index.js file so that it can be imported into the main app reducer:
There’s now no need to export the reducer, but we do need to import it somewhere so that our module bundler (e.g. Webpack) knows about it and includes it in the appropriate bundle.
There are a couple of ways to do this.
The reducer can register itself with the
Then, the module’s
index.js file can import the reducer without re-exporting it:
Another option is to move the registration of the reducer into the module’s
Either way works, but I lean toward the latter approach because it allows the reducer to stand on its own and only code outside of itself determines how to hook it into the main application reducer.
Note that I’ve used
createReducer from zeal-redux-utils here, but that’s an implementation detail. You can define your reducers however you want.