We'll dive into the selectors concept in the Zustand/Redux libraries and explore the different use cases for dynamic and static selectors.
Why do we need selectors?
Selectors are just pure functions that may take arguments and they returns a result based on those arguments.
If you want to know more about pure functions, check the following Closures, currying, function composition as your new friends article.
What selectors give us?
- No repetition in our state read logic.
- Less boilerplate.
- Rerenders reduction in components.
- Option to perform computations based on state change without additional rerenders.
No repetition with selectors
Imagine you want to change a property inside store. If it's just 3 places it's not a big deal, but what if it's 20 or more? In the example below we changed one property from user to user1.
Without selectors, we'll need to change 20 files. With them, it's only 1... It's important to know that we may use selectors in other application layers - not only components.
We don't need to repeat state.reducer.something. With selectors, we're hardly reducing repetition and code that we need to write to start working on feature.
If you're interested in what kind of layers you may create, feel free to check the following Comparing Redux with Zustand for state management in React article.
How selectors reduce the boilerplate?
Imagine you have 20 components in which you're reading the same property - the users.
To achieve the same result in Zustand:
As you saw thanks to selectors we reduced the boilerplate across components.
Rerenders reduction with selectors
Let's say we have 3 components. The Top, Between and Last. That's how they will be rendered:
Now, we'll implement a counter feature in Redux and Zustand. The Last component will read the value from the store, the component Between will just render children and the First component will schedule a interval. In every tick, the increment action will be dispatched.
Let's start from Zustand:
When we rerender this code in the main App component, we'll have the following console.logs. Watch the GIF to understand.
The rerenders are performant
Take a look at numbers or logs 👆. First and Between are logged only once, meanwhile the Last is rendering all the time! That's what we want.
Okay, but what about Redux? The result is exactly the same. Some APIs and implementations differ a little bit.
Performing computations based on state change
Let's say we want to perform a computation in every counter property change. It will be a simple Fibonacci number. Firstly, we need a function to calculate this number.
Now, we need a new selector.
This code will work, but there is a problem. The Fibonacci function will be executed all the time when the counter store property changes. We need to apply here a memoization technique - aka flyweight pattern. We can achieve it in Redux/Zustand with the same library - reselect.
Memoization is an optimization technique that is used to improve the efficiency of algorithms which requires repetitive computations. It's just caching.
With these changes Fibonacci function will be executed only, when we pass arguments that are different. It's cool - the previous computation will be stored and returned from the cache. It's fancy especially when we'll open the same component again - the next ticks will run much faster because computations are returned from the cache.
The dynamic selector is the one that makes our component rerender. Let's start with examples:
So, adding dynamic selector to this code will perform additional rerenders if the property in a state that we're selecting will change.
These selectors are the most used in React applications, but is there an alternative that will not cause a rerender? Yes!
Imagine a situation in which you're not using data from store in your JSX component code, but this data is required to perform some operations after the user clicks a button - redirection to different urls.
So, how we can achieve it without making a rerender? Let's create static selectors for Redux and Zustand. Firstly, the Redux.
Now, it's time for Zustand.
As you saw we avoided not needed rerenders and thanks to static selectors we just picked small slice of our state.
Little tweaks for better readability and naming - conventions
When I'm writing my applications I'm trying to prepare some conventions and then follow them in projects. It keeps my codebase consistent and allows me to back to IDE after a long break and still write code in a similar pattern. I'm putting these rules inside the repository and saving it in the md file. To be honest it can be any place - u2y.
Why I'm saying that?
Look at the following example with typical naming conventions that are used in Redux or Zustand.
Selectors code quickly will start to be repetitive and will contain the same prefixes... Usually, we're using selectors per feature so I'm wrapping them with one object and then reusing them in many places. From my perspective, it looks much better and it's easier to read but this is just an opinion.
So that's my proposal:
In addition, you may use the same approach for your Redux/Zustand actions. It's cool how it makes code modular and reduces repetitive prefixes before selectors and actions.
Full example to play with
Repository to check
If you want to play with this code go to the following repository.
Conclusions and thoughts
We explored the selectors concept in Redux/Zustand and now you know how to create dynamic and static selectors.
You learned that selectors are an excellent mechanism to reduce rendering impact in applications. They provide better readability, reduce the extraction of data logic from complex models, and allow us to refactor our state read logic faster.
I'm encapsulating selectors in objects for better readability and to reduce weird names. Of course, you may have different opinion, but I like to structure my code in a consistent way (in presented approach we did that). You may have your own approach - just be consistent and that's all.