by Martin Aliverti
First, let’s recap very briefly the work done so far, and then we’ll see what problems remain and how selectors can help us get rid of them. Or, go straight to the finish line and see why selectors are so cool.
React is a lightweight library that allows you to compose your app from smaller pieces called components, so that code is well compartmentalized and organized in small chunks of well defined behavior and scope.
Each component can be understood as the sum of two concepts, the state holding relevant values persisted throughout the different renders, and the view which is the graphical representation (render) of those values. When an event happens in the view -a button click for instance- the state is updated and the view reacts to reflect this new state. What could go wrong?
For small apps with little amount of components, or those which do not involve advanced interaction between them, React might be all you need. But as soon as you start having more complicated interactions between your components, and you want what happens in one component to affect the view of another, you start having callbacks and props passed to components just so that they can in turn pass them over to a child.
This means that you are coupling the intermediate components to behavior that does not belong to them. Solution? Redux.
In order to decouple all the components even further and avoid the callbacks and props handling by intermediate components, Redux provides a global store available to any component and a mechanism to update it. Views dispatch actions, which are interpreted by reducers, which then update small parts of the global state. Now, any view observing any updated piece of state can re-render its content.
So now components can focus just on rendering and behavior, having as inputs only the parts of the state it is interested in (local component’s state is still available if needed). No callbacks or props passed over to children. Life is good. Or is it?
We said components observe parts of the global state and re-render if those are updated. How does that happen? You connect them by referencing the path to the specific part of the state you want to observe.
In this case we are interested in the user’s birthDate and country, these props are now available for the component to use. So what’s wrong? Coupling the components to the store structure, that’s wrong. If we wanted to change the path of the birthdate, then we would have to change all of the components referencing it.
Say you have an ISO date string representing the birth date of a user stored in your redux store. In your view you want to show the age, since the ISO format would be of little use. Where should we transform the ISO date string to a meaningful value like 10 years?
In the View? The main problem here is that any transformation will be recalculated on every render. For a trivial date transformation (for which you normally would user moment anyway) this is no trouble, but think about filtering or ordering lists every time the component renders, and you might be well below optimal performance. Problems with this approach:
values calculated on every render
non-related updates trigger calculations
calculation takes place in every component displaying the value.
What’s more, if you need to make a business decision based on the calculated value, for instance, show an option if the user is over 21 years of age, the business logic will necessarily be placed in the view (since that is the only place where you actually know the value) and that violates the separation of concerns principle.
In the Store? The pro here is that by storing pre-calculated values we avoid the calculation on every render. Cool. But we end up with a store that is denormalized and structured according to the view. Again, storing the value age: 10 right next to the ISO date string might not be such a problem, but think of lists that can be filtered and ordered by different criteria; would you store the filtered list right next to the original one? That would be a bit of a waste of storage. So the problems now are:
store structured in correlation with the view
Selector is a pattern useful to solve the coupling problem between the redux store and the components. If we use reselect we also improve performance by memoizing the results of the selectors.
Selector are idempotent functions. They can compute derived data, allowing Redux to store the minimal possible state.
Selectors are memoized. Thus, they are efficient. A selector is not recomputed unless one of its arguments changes.
Selectors are composable. They can be used as input to other selectors.
Selectors sit right between the store and the view. This allows for an optimal storage structure while avoiding data transformation in the view. Moreover, since selectors can be memoized data transformation only occurs if any of the selector’s inputs were updated. If not, the last calculated value is returned right away.
In your mapStateToProps you will now have a reference to a selector return value, not to a path in the redux store. This means that if you need to change the way your store is structured, it has no impact at all in any of the views accessing it, because they will be doing so through the selector. There will be one single point of change, the selector itself. Awesome, right?
When you create a selector you take n inputs and combine them to calculate a return value. Such inputs can be references to the redux store, but can also be another selector, which allows for composition of complex business rules out of very simple pieces. Having base selectors that make no changes but only reference a path in the storage also leverages the power of the decoupling, because your domain logic, placed on composed selectors, will also be decoupled from the store structure.
Selectors are nothing more than idempotent functions at its core, which means that for any given set of inputs, the output will always be the same. This allows for dead-simple testing, and since we’ve already decoupled and composed the business logic, we can be confident that the important stuff is well covered.
Let’s see what an actual implementation of selectors looks like.
As you can see, we declare and create the selectors in CoolModuleSelectors.js using reselect, and then we import them in, and connect them to, our AwesomeComponent. This way the view gets everything pre-computed and the state remains independent of any representation of it.
If any change happens in the store structure and the path to the user data becomes state.anotherPath.user then we need to change only 2 lines, the declaration of birthDateSelector and countrySelector; the logic we’ve built on top of that and the way we render our data remain exactly the same.
Not everything is smiles and rainbows. We found out that organizing the selectors might be quite a challenge. Because they are so easy to write, you might be tempted to write the same selector more than once, or might not even know that such a selector is already available in some other part of your codebase. This kills the single point of change advantage so you should be careful.
Also, there might be selectors that serve to several components, possibly in different modules. I’m not a big fan of the commons place, but there might be times where you could end up with a CommonsSelectors.js to hold all of those.
Selectors are memoized, composable, idempotent functions which allow us to divide our business logic into minimal, testable and reusable chunks. By doing so, we completely decouple state from views and, by means of successive transformations, derive pre-calculated data to the views.
Have you tried selectors? Do you know of any other relevant advantage of using them? Have you had any problem we’ve not mentioned? Let us know what you think!