Introduction
React-Redux, although a little heavy, is still a wonderful library to work with. It takes care of a lot of things around state management and creates a more reactive UI. That being said, if not used carefully it can be a real performance killer for your app.
Redux's work can majorly be divided into two parts:
- Making changes to a central state
- Reacting to changes to that central state
(The above is a simplification. There are a lot of other extra features Redux provides)
The Redux store itself can be divided into slices. Logically, one can assume that all changes related to a feature are in that feature's dedicated slice. This provides a good separation of concerns and a cleaner code.
Whenever a redux store updates, all subscribed components will re-render. A component subscribes to a store using a selector. One costly mistake a lot of developers make while using redux is writing unoptimized selectors.
Basics
There are multiple ways to write a selector. For example, to read the value of the key title
from the counter
slice of the store, one can do any of the following:
const { counter : { title } } = useSelector((state) => state);
const { title } = useSelector((state) => state.counter);
const title = useSelector((state) => state.counter.title);
and even:
const { title } = useSelector((state) => {title : state.counter.title });
All the above might look the same and a developer might pick one or the other based on their preference. But all of them have different implications and performance costs.
How do selectors work?
Here is how the docs describe a selector:
One common description of selectors is that they're like "queries into your state". You don't care about exactly how the query came up with the data you needed, just that you asked for the data and got back a result.
All selectors are re-run after every dispatched action, regardless of what section of the state is updated. A selector returns a value. If that value is different from the previous value returned by the selector the component re-renders. Simple as that.
How the two values are compared is how most things in the React or Javascript world are compared - using ===
referential equality.
With this information, we can understand how few of the selectors mentioned above might kill the performance of any app.
For example, a selector like the one below will cause a component to rerender no matter which part of the redux store is updated:
const { title } = useSelector((state) => {title : state.counter.title });
This is because everytime a new object is being returned. {title: 'Hello World'}
and {title: 'Hello World'}
can never be the same.
The same goes for :
const { counter : { title } } = useSelector((state) => state);
The most optimal way to write the selector would have been:
const title = useSelector((state) => state.counter.title);
Components using the above selector would only re-render when the title
string changes its value. This is what the component expects too.
How one groups and writes their selector to pick relevant data from the store balancing readability, cost, and reusability is up to them. But, in cases where a single property from some slice is needed, writing a specific selector can be a real performance booster compared to their multiple other options.
Bonus: A demo
Here I have created a small demo to show the extra re-renders caused by costly selectors.
I have two slices counter
and counterB
in my store of the same shape.
{
count: 0,
title: "",
}
There are buttons on the UI to update the state with redux-actions. Looking at the logs it can be seen that updating one part of the state can end up re-rendering a totally unrelated component.
Explaining the whole code here and giving all the details will be futile as it can be better understood by playing around with the code and logs. I hope you find time to do so:
Top comments (0)