Modular Redux - a Design Pattern for Mastering Scalable, Shared State in React
How to write React+Redux that scales to very large applications, with dramatically less code
Single page apps require a new way of thinking about web design. Instead of storing client session state on the server, more and more is stored in the client. State is one of the trickiest things to manage in a growing code-base. It’s particularly tricky when the state is shared across multiple components in your client-side application. Thankfully there are tools that can help. Shared-state libraries help with understanding and debugging the state across multiple components.
In this article I’m going to focus on Redux, one of the most popular solutions to managing shared client side state. Redux really shines when it comes to understanding and debugging shared state:
- Redux’s serializable, immutable, shared state is easy to inspect, persist, and restore
- Redux’s serializable, atomic, dispatched updates are easy to inspect, record and playback
Redux is the de facto standard for React shared-state, and there’s good reason. With centralized serializable state, serializable actions, atomic updates in the form of reducers, not to mention great middleware, there’s a lot to recommend Redux. State predictability and debuggability is the name of the game, and Redux is a best-of-class solution to shared state management… almost.
Redux is a pain to use and doesn’t scale. Even on the smallest projects it takes an excessive amount of ‘boilerplate’ code to get up and running. On large projects, it only gets worse. Redux’s complexity scales poorly with project size. Except… it’s not actually Redux that’s the problem. It’s the way it is used. Both the traditional Redux and newer redux-toolkit design patterns are inherently complex and don’t scale.
Thankfully, Redux can be used in a way that is both easy and scales. We can have our cake and eat it too. It’s possible to have all the wonderful properties of Redux and do it with svelte, clean, scalable code. We just need a change of design pattern.
Redux is a best-of-class solution to shared state management
The current state-of-the-art redux-toolkit was a good start, but in this article I’ll show how to take your Redux code to the next level. I will show how to write Redux code that not only scales to very large applications, but I’ll also show how to reduce your Redux-related code by half. Best of all, I’m going to do it with no new dependencies. I’m only going to use plain React and Redux.
The key is modular design.
This article is organized in the following sections:
- The 5 Essential Elements of Modular Software Design
- What is Modular Redux?
- To-Do-List Tutorial Using Modular Redux
- Comparing Modular Redux and Traditional Redux
- Comparing Modular Redux and React Toolkit
- Streamlining Modular Redux with Hooks-for-Redux (H4R)
- Modular Redux Scales - It’s also Just Easier
- Get Started
The 5 Essential Elements of Modular Software Design
This article is a follow-up to my previous article on the elements of modular design. I’ll be referring back to it from time to time. I recommend reading it if you want to learn more about the principles of scalable, modular design:
Where the previous article applied to software engineering in general, this article is all about the nitty-gritty of real-world React-Redux applications. My goal here is to show how to write better, more scalable Redux code, and also to use Redux as a real-world case-study of the benefits and techniques of modular design. I’ll compare three different real-world applications of increasing complexity showing for each in turn how Modular Redux is vastly superior to the status quo.
What is Modular Redux?
Modular Redux is the result of consistently applying good, modular design to applications using Redux. Modular Redux is a design pattern. It’s a re-usable solution to using Redux in real world applications. My focus is on React applications, but Modular Redux can apply to any platform.
From the point of view of modular design, there were four main opportunities for improving Redux design patterns:
- Encapsulate Redux modules with respect to each other: Reducers typically have access to all redux state and all dispatched actions. This means every part of the application that touches Redux is potentially dependent on any other part. This is why global variables are so bad, and why Redux, when used improperly, can get out of control. Redux is a global state, but it doesn’t have to be used that way. We can modularize Redux slices so each slice only has access to its own sub-state and actions.
Redux is a global state, but it doesn’t have to be used that way.
Consolidate all code related to one Redux slice into one module: All reducers, action-types, subscriptions, getters and dispatchers can be consolidated for each Redux slice resulting in code reduction, stronger encapsulation and greatly reduced inter-dependencies.
Isolate Redux from components: Components should not be directly dependent on Redux. If we need to refactor how we manage shared state, we shouldn’t have to rewrite every component that uses that shared state. Instead, we’ll implement the simplest possible API for each Redux-module, taking care not to expose the fact that we are using Redux as the underlying implementation.
Minimize component shared-state dependencies: Often I see people adding Redux dependencies to a parent component only to do nothing with them other than pass them to children components via props. This can make sense in scenarios where the sub-component is reused with different state-sources. However, it’s overkill if the sub-component only accesses one slice of Redux state. In general, if a component doesn’t directly use or update shared state, it shouldn’t be dependent on it. (Build your components with ZEN - zero extra nuts.)
If a component doesn’t directly use or update shared state, it shouldn’t be dependent on it.
To see how apply modular design principles to Redux, and get the most out of the opportunities listed above, let’s do a tutorial.
To-Do-List Tutorial Using Modular Redux
To-Do-List Tutorial Files
This example consists of 5 files. These are the app’s modules. I’ll explain them one at a time:
The root file of the app is about as simple as it can be. We are importing our root component, ToDo, and rendering it as a child of the ‘root’ element. Notice there is no Provider nor any other Redux-related dependencies. There is no need for index.js to be dependent on Redux, so it shouldn’t be. This is one of the essential elements of modular-design: minimize inter-dependencies.
The main component shows a list of ToDo items returned from useList. It also presents a simple HTML form for adding new items via addItem. The all to useState and the createNewToDoItem function are used to capture the current value in the DOM’s input field.
Experienced Redux developers will notice an important difference here compared to other Redux design patterns. We are importing simple methods directly from our redux/list module. These are simple functions. There is no manual dispatching, action-creating or other complications. This component has no dependencies on Redux at all. It only accesses an API which provides exactly what the component needs to function and nothing more.
The redux/list module’s API is the simplest solution possible given the needs of the application:
useList returns an array of items
addItem takes a single item as input, an object with a single ‘text’ property
ToDoItem is the only other component in this simple app. It, too, imports exactly what it needs from redux/list and nothing more: deleteItem. You might notice that we didn’t pass deleteItem into ToDoItem from ToDo as is common in other Redux patterns. Doing that makes two components dependent on deleteItem instead of only one.
This module is where the magic happens. It solves a well defined problem, manage the to-do list, with the simplest possible solution: an API consisting of three functions with the simplest possible inputs and outputs. Defining the essential problem and the simplest solution possible are two essential elements of modular design.
If we stubbed the list module, it might look like this:
Now I’ll go through the actual implementation from top to bottom, explaining it chunk-by-chunk:
At the top of the file we define the basic logic of the list state:
- storeKey defines the name of the Redux store slice where the list will live
- getUniqueId takes a list of to-do items and returns an id not in the list
- initialState pre-populates the list with two to-do items
- reducers is a map from action-names to functions. These reducers take in the current state of the list-slice (importantly, not the whole Redux state), and a payload. Then they return a new list with the change applied.
Notice that reducers do not have access to the full Redux state, yet another essential element of modular-design: encapsulation.
Next are a few helper functions. These make it easy to interact with just our slice of the Redux state instead of the entire, global state.
getState returns the current list
subscribe takes a function as an input. That function gets called with the current list whenever the list changes. Subscribe also returns a function for unsubscribing.
Finally, we replace our stubs with working implementations:
- useList uses standard React hooks to re-render any component it is used in, returning the current list every time it’s called.
- addItem dispatches a Redux action to trigger the addItem reducer. Notice that the dispatcher only takes an item as input; that’s all it needs to know to do its job.
- deleteItem also dispatches a Redux action, this time to trigger the deleteItem reducer.
Notice that addItem and deleteItem, the dispatchers, only take exactly what they need as inputs - the item to add and the item to delete respectively. There is no need to pass in action-names nor construct action data-structures.
Last, we need to add our reducers to the redux store. This may seem a little backwards compared to the traditional Redux pattern. Usually you define all the reducers and then call createStore, passing them in. In order to minimize dependencies, we moved everything related to this Redux slice into one file. Therefor we need to have access to the store in the same file where we define the reducers. This is why in Modular-Redux, the Redux-modules register themselves with the store rather than the other way around.
As it turns out, this helps us not only further reduce dependencies by eliminating the need for the traditional Redux reducers file (see below), the self-registration method of defining Redux modules also allows us to do cool things like lazy loading and hot reloading.
Note: If an application needs control over which store a modular Redux slice is registered with, simply wrap the entire contents of this file in a function that takes the store as an input. Then invoke the function with the store you want to bind it to.
The last file of the to-do list example defines the Redux store. It creates a basic store with an injectReducer method. This pattern is described in detail on the Redux.js website:
If you haven’t already, try the working tutorial example and explore the code:
This example seems almost trivially simple. The key is consistently applying the 5 elements of modular design to each file: define a single, narrow and focused problem, create a well-defined, complete and minimal solution, ensure watertight encapsulation, implement a correct, performant and minimal foundation, and do it with minimal inter-dependencies.
Comparing Modular Redux and Traditional Redux
Here’s why modular Redux really matters: it can reduce the complexity of shared state by two to three times. Let’s compare two equivalent implementations. One is basically what we just finished in the tutorial - with styling added. The other is functionally identical but written in the traditional redux way:
Modular redux slashes the dependencies between modules. The traditional implementation has 19 dependencies between modules:
Note: CSS, react, and react-dom imports are omitted since they are identical in both projects.
- appActions depends on redux because it implements part of the redux dispatch API.
- appReducer similarly depends on Redux since it implements the redux-specific reducer API.
- further, App depends on Redux since it explicitly uses the Redux dispatch API.
In all three of the above cases, if you swap out redux, you’d have to complete rewrite or replace these files.
- appActions depends on appReducer since every action in appActions must exactly parallel the reducers defined in appReducer. If you add or change your reducers, you’ll also have to update appActions.
- ToDoItem and ToDo both implicitly depend on appActions since they implicitly use appActions. If you change the signature of any of those actions, ToDoItem or ToDo will need to be updated.
Now, compare that with the Modular-Redux solution’s mere 7 dependencies:
There are zero indirect dependencies. Each module can be fully swapped out without needing to update other modules. The relationships are clear and easy to understand. Minimized dependencies maximize code scalability. Modular Redux dramatically cuts down on the number of files and lines of code and has 63% less inter-dependencies.
Modular Redux vs Traditional Redux:
- 2.7x improvement: 7 dependencies vs 19
- 1.8x improvement: 5 files vs 9
- 1.6x improvement: 104 lines of code vs 170
Comparing Modular Redux and React Toolkit
In the tutorial above I compared Modular Redux with the standard Redux pattern. I did this because it is the Redux pattern most people are familiar with. This is somewhat unfair, however, as the official Redux recommendation is to use the newer Redux Toolkit. Redux Toolkit does make good progress over the traditional approach, but let’s see how Modular Redux can bring things to the next level. The Redux Toolkit website provides a basic tutorial, an intermediate tutorial and an advanced tutorial. I’m going to skip over the basic tutorial and dive directly into a detailed comparison using the intermediate and advanced tutorials.
Intermediate Tutorial: To-Do List with Filter
This version of the To-Do list uses two Redux slices. In addition to adding and listing to-do items, you can also filter the item list.
First, let’s start with the Redux-Toolkit-based implementation. I recommend opening the CodeSandbox to get a frame of reference:
- interactive Redux Toolkit solution (codesandbox)
- Redux Toolkit solution source (github)
- Redux Toolkit Intermediate Tutorial Documentation
Let’s jump right into the Redux Toolkit dependency diagram:
There’s quite a bit going on compared to our first tutorial. There are three external dependencies on redux-related libraries instead of one: redux-toolkit, react-redux and redux itself. While there are a lot of interdependencies, there were only two implicit ones: Link and TodoList use the filterSlice and todoSlice APIs indirectly, respectively.
Redux-Toolkit does a good job of eliminating the need for multiple files to define each redux-slice. There is just one file for the to-do list and one file for the filter. However, Redux-related dependencies are still littered throughout the components. Now let’s look at the Modular-Redux solution for the same To-Do-List-with-Filter:
The Modular Redux dependency diagram:
The above diagram shows what a dramatic improvement modular design can bring to a project. We cut the number of dependencies from 27 down to just 13.
You may notice there are less components in this version. As I was rewriting the code, simplifying it, additional simplifications suggested themselves. This is why it is so important to always strive to make things as simple as possible. Complexity breeds complexity. If a system is overly complex, it will tend to grow even more complex. The complexity itself necessitates ever more complexity to manage it.
Complexity breeds complexity.
When should a module not be a module? The main sign a module might not be necessary is when it has only one other module dependent on it. In that case, one needs to determine if the module is substantively different from the module that depends on it, and it the code is simpler or more complex with the sub-module.
In this tutorial, FilterLink and Link were intimately tied up with Footer and the filter options and weren’t being used anywhere else. I was able eliminate 38 lines of code, two files and only add 3 lines of code to Footer. If moving code into modules causes a 12x increase in code-size, it’s probably not the best solution.
I also added one module. There was some redundant code in the filter and todo modules. Since the code was used in two different places, I was able to create a new module, modularRedux, simplify both the original modules and reduce overall code-size.
Intermediate Tutorial—Modular Redux vs Redux Toolkit:
- 2.1x improvement: 13 dependencies vs 27
- 2.0x improvement: 193 lines of code vs 393
- 30% improvement: 10 files vs 13
Advanced Tutorial: Github Issue Browser
The final Redux Toolkit tutorial is a styled Github issue browser. It adds asynchronous requests, TypeScript, limited testing, hot-reloading, styling and has four different redux-slices: issuesDisplay, repoDetails, issues and comments.
You can learn about the Redux Toolkit implementation here:
- interactive Redux-Toolkit Advanced Tutorial (codesandbox)
- Redux-Toolkit Advanced Tutorial source (github)
- Redux-Toolkit Advanced Tutorial Documentation
The module-dependency diagram significantly more complex than anything we’ve looked at so far:
The Redux Toolkit Advanced Tutorial implementation had a ton more implicit dependencies. For example, showIssueComments is imported in App, but it is passed through IssuesListPage through IssuesList before it is finally actually used in IssuesListItem. It also contains a complicated set of inter-dependencies between the Redux modules themselves.
Now let’s look a the Modular Redux implementation:
As you might expect, Modular Redux dramatically decreases the number of inter-dependencies. The total dependencies decreased from 63 to 34, but that only tells part of the story. This time the React modules were already optimal, so the component dependency structure is identical in both implementations. All 28 eliminated dependencies came from improving how Redux is used. The Redux-related dependencies decreased by more than half from 48 to just 19.
The right-hand side is still pretty busy, but even without knowing the contents of the files, the diagram helps illuminate what’s going on in the application. For example comments, issues, and repoDetails all update when issuesDisplay changes, and that’s clearly reflected in the dependency diagram.
You’ll notice there is no dependency on redux-thunk. Each of the four Redux modules manage their own asynchronous loading internally. Subscriptions and promises sufficiently and elegantly manage the asynchronous requests. There is no need for the added complexity of thunks.
I also added TypeScript support for modularRedux. Typing works very well with the Modular Redux pattern. You’ll notice there is very little explicit typing in any of the Redux modules. Eliminating the pass-through, indirect dependencies in the components also cleaned up huge swaths of unnecessary typing code.
Advanced Tutorial—Modular Redux vs Redux Toolkit:
- 2.4x improvement: 20 redux-related dependencies, vs 48
- 1.8x improvement: 35 overall dependencies, vs 63
- 1.7x improvement: 685 lines of code, vs 1169
Redux Toolkit’s Missed Modular Design Opportunities
Of the four Redux modularization opportunities I discussed above, Redux Toolkit only addresses one and a half out of the four:
- addressed: Encapsulating slices with respect to each other. Each reducer only handles the state of the slice, not the entire state. Reducer-routing is handled automatically, so a particular slice’s reducers aren’t allowed unfettered access to all actions.
- partially addressed: Redux Toolkit does consolidate reducers, action-types and action-creators into one file, but it still leaves dispatching, subscribing and getting the current state to the individual components.
- missed opportunity: Redux Toolkit still fully mingles Redux-specific code throughout every component in the project. This is such a disaster that, when rewriting the Advanced Example with Modular-Redux, I ended up rewriting it from scratch rather than refactoring the existing code. This is why it’s so important for modules to not expose their own dependencies. Redux-Toolkit’s slice-modules expose their Redux dependency making it near impossible to refactor on anything but trivial projects.
- missed opportunity: Both the intermediate and advanced Redux Toolkit examples continue to repeat the broken pattern of importing dependencies in a parent component only to pass them to a child component without ever using them. This isn’t a direct failure of Redux Toolkit, but by repeating this error in the tutorials they are propagating bad design to the broader Redux community. The Advanced tutorial is especially guilty as the showIssueComments function is actually passed not to a child component, but all the way through to a great-grand-child component before it is actually used creating four dependencies when one would have sufficed.
While Redux-Toolkit is better than the traditional Redux design pattern, but it comes at the expense of adding its own complexities. It adds new concepts but doesn’t successfully abstract away Redux base concepts. The result is you have to deeply understand both to successfully use the toolkit.
Streamlining Modular Redux with Hooks-for-Redux (H4R)
If you decide to use the Modular Redux pattern, I recommend using hooks-for-redux. In some of the diagrams above there is light-grey rectangle with ‘H4R’ in the lower right-hand corner. H4R is a drop-in replacement for those rectangles. It is a tiny library (currently just 90 lines of code). It implements the common parts of the Modular Redux design pattern in a well-tested and fully TypeScripted way.
I’ve implemented each of the tutorials above using H4R. You can view them here:
- To-Do Tutorial with Modular Redux and H4R:
- RTK Intermediate Tutorial with Modular Redux and H4R:
- RTK Advanced Tutorial with Modular Redux and H4R:
Read more about hooks for Redux:
Modular Redux Scales - It’s also Just Easier
Redux is a very simple idea. It lets you define shared state, methods for atomic updates and subscriptions to changes. Where Redux really shines is the wealth of middleware available you can attach to a redux store to persist, restore, playback and inspect your state. Because of all those reasons, Redux should scale very well, but it typically doesn’t. The problem isn’t Redux itself. The problem is the recommended ways of using Redux introduces a ton unnecessary complexity.
The Modular Redux design pattern dramatically simplifies using Redux. While making things easier is great, Modular Redux shines most as you scale your application. Modular Redux kept the complexity under control for each of the three applications presented above even as they increased in size. This comes from the systematic application of the essential elements of modular software design across all aspects of Redux. Modular Design helps ensure each module solves a focused problem, with a well-defined, complete and minimal solution, using watertight encapsulation, supported by a performant, correct and minimal foundational implementation, and most of all, while minimizing overall modular inter-dependency.
With Modular Design, Redux becomes a Best-of-Class solution to React shared state. You can have Redux’s centralized serializable state, serializable actions, atomic updates, great middleware and you can do it with svelte, clean, scalable, modular code.
With Modular Design, Redux becomes a Best-of-Class solution to React shared state
To get started, I recommend either taking one of the examples above, or you can dive into the Hooks-for-Redux Tutorial which is a little more detailed and will guide you to towards the best way to use Modular Redux in your application.
- Part 1: The 5 Essential Elements of Modular Software Design
- More about H4R - recommended library for building modular-redux applications:
How I Eliminated Redux Boilerplate with Hooks-for-Redux (H4R)