There are three key points when it comes to make your Redux code scalable and maintainable:
- Using action creators: 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 connected 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.
- Using multiple reducers: you can reduce the whole state of your application using a single reducer function, but that will end in a huge
switchstatement 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.
- 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
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
And if you change the way your
todos reducer stores the information, you just have to update the selector functions exported from
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
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 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
The root reducer module uses
bindSelectors to bind all the selectors defined for the
todos reducer, providing the
Finally, the component uses the exported selectors from the root reducer module.
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 (this post by Randy Coulman and the one 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.