Face uncertainty by decoupling Redux state
Redux is one of the most widely-adopted and used tools today. Indeed, the idea of using a single state tree and simple, trackable flow of events through reducers was easy to understand and debug.
However, developers find it annoying to deal with relatively high amount of boilerplate, and as always, when you go beyond the basic concepts described in tutorials and demos, things start to look scary and complex.
Redux creator Dan Abramov mentioned that not every application actually requires Redux, and many can manage without explicit state management tool or by using alternatives. Moreover, there are speculations about a new type of state management tool coming soon.
In reality, there are many applications that are already invested in Redux. Moreover, if Redux is a good fit for big applications, how do we manage the complexity of facing changes and evolution of such apps effectively?
In this post I will try to show how decoupling and encapsulating Redux state can help us to deal with unpredicted changes while keeping components simple and allow easy testability for app components and data layer.
Motivation
If you are already motivated enough to have your state tree decoupled and encapsulated, skip this part and go to the next chapter.
When all the requirements are well-defined in advance, it might be clear and easy to build the state tree, but what if things are not clear from the very beginning? What if you are gradually creating UI and the next feature is still not clear? What if you are not sure about the shape of the state? Will you use ImmutableJS or reselect, will your state be normalized?
Uncertainty doesn’t allow us to plan and implement ideal state, so most chances it will change in future — and we should be prepared.
State tree shape can change
It is annoying to change mapStateToProps
every time you decide to move or rename some part of state tree. The change would require refactoring of every component that consumes the changed part of the state.
Consider the following example, you start with a simple state tree, which has users
and tasks
lists, e.g.:
{
users: [{ id: "user01", name: "John" }, { id: "user02", name: "Bob" }],
tasks: [{ id: "task01", title: "Wash car", assignee: "user01" }]
}
You have 3 views, each consumes some part of the tree:
Users
— lists all users, consumesusers
from redux state, it expects to get an array of users in its props:props.user = [...]
Tasks
— lists all tasks, consumestasks
from redux state, it expects to get an arrays of tests in props:props.tasks = [...]
TasksByUser
— lists all tasks by a user, consumesusers
andtasks
from the state and expects to receive list of users with their tasks included:props.userTasks = [{ id:..., tasks: []}, ...]
Sweet! Now let’s say you’ve decided to add a new top-level brach ui
— people say it might be good to keep local components’ state in redux.
We don’t know what additional global branches we’ll need (may be env
?), so we decide to scope all “business”-related state under app
.
Our state tree now looks:
{
app: {
users: [...],
tasks: [...] }, ui: { ... }
}
Can you smell the problem? All the views require refactoring — 👎🏻.
State tree entities structure can change
Next challenge! Let’s follow the best practices and have our state normalized. The new state would have the next structure:
Notice that we have changed users
and tasks
branches — the entities are replaced with keyed objects {}
, instead of arrays []
.
The components have to be refactored again! We need to rewrite mapStateToProps
in each component (again) in order to deal with the new format of the data — 👎🏻.
More motivation
Eventually you’ll add more components — they will represent new state entities, or relations between state entities. You’ll want, though, to keep your state “minimal”. That means that only essential data lives in the state tree — all other derived pieces of information will be calculated based on the minimal state, and recalculated when the minimal state is changed.
The classic example is to have a visibilityFilter
and items
as part of the minimal state, while visibleItems
are derived every time the related fields of the minimal tree are changed.
That approach prevents duplication of data and makes your data layer “reactive” in the manner similar to ReactJS components.
Libraries like reselect (or re-select) will help you to create pure memoized functions for computing the derived state automatically and efficiently.
You need to manage the derived state efficiently — maximize code reuse, allow testability and easy management.
Even more motivation
When connect
ing our state to a component, we extract different fragments of state tree to compose a convenient data representation (either in mapStateToProps
or in render
). If you find yourself in such a situation — most chances this logic should be extracted from the component and be part of “derived” state.
Your components should be agnostic to implementation details of the state — mapStateToProps
just receives state
and extracts slices that are required for rendering. The component shouldn’t know whether it is a derived data or part of the minimal state.
Moreover, ideally, you would never want to modify mapStateToProps
due to a change of state shape or adding convenience / management tools.
By doing that we prevent duplication of functionality and keep components “dumb”, simple, modular and elegant.
Are we motivated yet?
Expecting changes
Let’s see how can we can use some techniques to easily modify state and keep the complexity related to data management out of components.
As example, consider an application with the following entities in the tree:
users
— list of users with nested list of books readbooks
— list of bookscomments
— list of user comments for a book
The application has the next views, which are implemented in React components accordingly:
AllUsers
— list of all users with short summary of comments and booksAllBooks
— list of all books with a shoer summary of readers and commentsUserDetails
— single users details with a detailed list of all books read and user’s comments for each bookBookDetails
— single book detailed view with a detailed list of readers and comments
The source code of the app used for demonstration is available at https://github.com/agoldis/decouple-redux-state. The repository has several branches that contain changes I am applying to the code.
There could be many implementations for this application. The particular implementation I suggest aimed to display the decoupling techniques. I assume my intelligent reader will apply the example on bigger real applications.
We need to compose different entities of the state in order to create proper data structures for convenient rendering of the views.
Check out this branch to examine the application https://github.com/agoldis/decouple-redux-state/tree/01-initial-project
Each component defines its own mapStateToProps
method and uses react-redux
's connect
utility to get updates. I am listing the implementation of each such method — you can see that different components require different representation of the state, combining its pieces to get a convenient structure.
For this simple application it’s not a problem to keep these mapStateToProps
together with the components, but remember that we are preparing for changes — and even a small state structure change would require a lot of refactoring; when application grows, we may end up having many components and much more refactoring.
Obviously, we’d want to extract the logic defined in mapStateToProps
. But how to make it available to all components? May be, we’ll just move it to the state itself?
Let’s look closer how do we creating a store — the method createStore
accepts the following arguments:
createStore(reducer, [preloadedState], [enhancer])
We’ll use the storeEnhancer
to enhance the store and silently substitute the original state
with our own:
A store enhancer is a higher-order function that composes a store creator to return a new, enhanced store creator. This is similar to middleware in that it allows you to alter the store interface in a composable way.
A store enhancer has the following form:
const storeEnhancer = next => (reducer, preloadedState, enhancer) => store
The enhancer accepts a single argument — the original createStore
, thus may be compose
'd. To replace the original state, we’ll enhance use the following enhancer enhanceStore
:
We save the original method store.getState
and replace it with () => deriveState(_originalState)
.
Let’s improve the example above — we’ll use an enhancer that is a composition of:
applyMiddleware
(that is used to wrap store’sdispatch
method)- our newly introduced enhancer
enhanceStore
We will use compose
method supplied by redux-devtools-extension, if present:
The enhancer enhanceStore
was moved into a separate file:
The interesting part is deriveState
— that is the method that accepts the original, non-modified state
and adds new deriver fields. The fields are only visible to components that use getState
:
You can notice that the methods booksSummary
and userSummary
are extracted from the original components Books
and Users
accordingly, I skipped the rest of mapStateToProps
methods from BookDetails
and UserDetails
to keep the example concise.
The implementation of deriveState
may be extended in many ways to add new, derived state fields that may be used by components (or reused by other derived methods).
We’d be careful, though, and remember to not mutate the state!
Check out this branch to examine the application after the changes https://github.com/agoldis/decouple-redux-state/tree/02-enhance-store
Experienced reader may say that having additional functionality (method members or getter) might violate the recommendation of having a serializable state.
“Serializable” means that the we can easily:
Dan Abramov’s quote: serialize, record and replay, hydrate and dehydrate
Although our enhancer only produces new state when getState
method is invoked, and that is not usually an efficient way of serializing the state (it’s better to have it done via another enhancer), we can hide the new computed properties from enumeration, by doing that we prevent the properties from being serialized and comply with the recommendation by setting enumerable: false
.
For demonstration, I have connected redux devtool and implemented “remove book” action — we can time-travel and jump from state to state:
Unfortunately, non-enumerable properties have some associated inconveniences, for example, they are not extracted when spread
operator is applied:
The Rest/Spread Properties for ECMAScript proposal (stage 4) adds spread properties to object literals. It copies own enumerable properties from a provided object onto a new object.
The enhanced store with the derived state allows us to extract mapStateToProps
complexity from components — that keeps components simple, allows easy testing of both the components and the methods used to compute the derived state. Moreover, we can modify the original state but keep the changes hidden from components. Let’s try to see how we easily modify the state without additional refactoring.
Move users
, books
and tasks
to a different branch
We have decided to move all business-related logic to app
branch. No problem! We just define new properties that get the data from new location — state.users
becomes state.app.users
, and state.tasks
becomes state.app.tasks
. No changes are required — the components are not aware of state structure modification.
At the listing above we create getter
s for the original state properties books
, comments
and users
. Although they were moved within app
branch of state:
const initialState = {
app: {
comments: { /* ... */ },
users: { /* ... */ },
books: { /* ... */ }
}
}
none of the components or other methods that computed derived state require refactoring.
Check out this branch to examine the application after the changes https://github.com/agoldis/decouple-redux-state/tree/03-use-app-branch
Using selectors
Now let’s try to use reselect to memoize the “derived” data and prevent expensive recalculations. The point is that modification and/or addition of new tools that help us to manage the state and optimize performance are invisible to the components.
We start by creating 2 simple selectors: usersSummary
and booksSummary
, they will be used to prevent recalculation of the derived state when the original state fields that are required for the calculation are not changed.
Did we need to touch our components? No! We have added a new state management tool, but components are not aware of its usage — they do not import any of selectors and not aware of their existence!
Check out this branch to examine the application after the changes https://github.com/agoldis/decouple-redux-state/tree/04-use-selectors
The code presented in examples for creating the the derived state might be reorganized and improved, of course, for example, alternative approach could be to return a completely new object every time deriveState
is called — that will encapsulate the state completely! Take a look at this example:
For clarity, the selectors were moved to a distinct file selectors.js
and imported.
Check out this branch to examine the application after the changes https://github.com/agoldis/decouple-redux-state/tree/05-state-object
Summary
By using this simple technique we achieve a great degree of decoupling of the state (and data management logic) from components. The benefits are:
- easy refactoring
- simple, testable components
- testable data layer logic
- slim “minimal” state
You can find the source code with fully-functional code of the application I have used while writing this post at https://github.com/agoldis/decouple-redux-state. The branches are used to track the changes:
- 01-initial-project — the initial project with state not decoupled from the components
- 02-enhance-store — introduction of store enhancer
- 03-use-app-branch — we are changing the structure of our state by moving all entities to
app
branch - 04-use-selectors — we are using reselect to compute the derived state effectively
- 05-state-object — demonstration of an alternative approach of organizing deriveState method and encapsulating the state
Read more
Want to have even more decoupling of your app? Check out those great articles:
- Selectors in Redux are a MUST by Riccardo Odone
- 10 Tips for Better Redux Architecture by Eric Elliott
- Five Tips for Working with Redux in Large Applications by AppNexus Engineering
- Encapsulating Redux Tree by Randy Coulman (@randycoulman)
- Encapsulation in Redux: the Right Way to Write Reusable Components by Tomáš Weiss