Scalable Redux: decoupling selectors from the state shape

Asís García
May 13 · 3 min read
Photo by on

There are three key points when it comes to make your code scalable and maintainable:

  1. Using : an action creator is a function that creates an action (mind blowing, I know 😄). It hides the actual shape of an action, so your “client” code (for example: a React component) can dispatch actions without worrying about how they actually look. This lets you change the way you define your actions without affecting the code using them.
  2. Using : you can reduce the whole state of your application using a single reducer function, but that will end in a huge switch statement processing all sort of unrelated actions. Updating each part of the state tree using a different reducer leads to code which is simpler, easier to understand and easier to maintain.
  3. Using selectors: a selector is a function you can use to read information from the state tree. You pass the whole state to the function and it returns the relevant data, effectively isolating the code which needs the state (again, your connected React component) from the code managing the state (the Redux store and reducers).

If you follow key point number 2 in the previous list, you’ll end up with a state tree where each state branch is handled by a different reducer:

Now suppose you want to get the list of todos and use it in some React connected component:

This component knows too much about the state. If you rename the state branch were the todos are stored (say from todos to todosList) you also have to change this and every other component accessing the state. This is a pretty stinky code smell!

That’s why you should always use selectors to access the state info (key point number 3). You can define and export a selector function when you create the store:

And use it when you define your connected component:

Now your component is effectively isolated from any changes to the state shape. But what about the selector function itself? If you keep it like this, you just moved the problem from the connected component to the selector function: it still knows too much about the state shape.

Reducer-scoped selector functions

Selector functions defined at the root reducer level should only know about the place in the state where each combined reducer is placed. This is true for each level in your state tree. At each level, you extract data from that level and forward it to the next one, using “reducer-scoped” selector functions:

This way, if you change where the todos reducer is “combined”, you just have to change the selector function exported from store.js:

And if you change the way your todos reducer stores the information, you just have to update the selector functions exported from todos.js:

Reducing boilerplate

With this approach, the code defining the selector at the root level is mostly boilerplate. You get the right slice of state and handle it to the next-level reducer:

If you want to rename todos to todosList as before, you have to change it in every selector. It’s not a big deal, as the selectors are collocated with the root reducer, but we can do better. Using a utility function you can define every selector for the same reducer at once.

The module where the reducer is defined exports a bindSelectors function. bindSelectors receives a function (slicer in the example code) that knows how to get this reducer’s branch from the global state. Finally, bindSelectors returns a bunch of selectors that use the slicer function.

The root reducer module uses bindSelectors to bind all the selectors defined for the todos reducer, providing the slicer function.

Finally, the component uses the exported selectors from the root reducer module.

Summing up

While almost every article about Redux talks about selector functions as a way to decouple components from the state shape, I’ve seen very little written about decoupling the selector functions themselves ( by Randy Coulman and he references by Jack Hsu are the only stuff I found about the subject). Having worked in projects with more than 100 selectors from 30 different reducers, I’ve come to appreciate this kind of stuff.

I hope the approach described in this story will help you build more scalable Redux applications.

Trabe

We are a development studio. We use Java, Rails, and JavaScript. This is where we write about the technologies we use at Trabe.

Thanks to Martín Lamas.

Asís García

Written by

Developer @Trabe

Trabe

Trabe

We are a development studio. We use Java, Rails, and JavaScript. This is where we write about the technologies we use at Trabe.