Face uncertainty by decoupling Redux state

Andrew Goldis
10 min readApr 29, 2018

--

Photo by Florian Klauer on Unsplash

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, consumes users from redux state, it expects to get an array of users in its props: props.user = [...]
  • Tasks — lists all tasks, consumes tasks from redux state, it expects to get an arrays of tests in props: props.tasks = [...]
  • TasksByUser — lists all tasks by a user, consumes users and tasks 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 connecting 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

Dealing with uncertainty

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 read
  • books — list of books
  • comments — 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 books
  • AllBooks — list of all books with a shoer summary of readers and comments
  • UserDetails — single users details with a detailed list of all books read and user’s comments for each book
  • BookDetails — 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.

Preview of the sample application https://github.com/agoldis/decouple-redux-state/tree/01-initial-project

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:

Using store enhancer to add properties to state https://github.com/agoldis/decouple-redux-state/tree/02-enhance-store

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’s dispatch 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:

Time traveling with derived 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.

Using derived state getters to hide state structure change

At the listing above we create getters 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.

Using selectors to in decoupled state to prevent calculation of a derived state

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:

--

--

Andrew Goldis
Andrew Goldis

Written by Andrew Goldis

I like coffee, web development and simple code. https://agoldis.dev

No responses yet