title | date | category | ||
---|---|---|---|---|
How to make useSelector not a disaster |
2020-09-13T09:00:00.009Z |
|
Disclaimer: We will focus on useSelector
itself in this article, rather than third-party libraries like reselect
, because it's out of scope of the article.
Now, before talking about useSelector
, we need to know some background. Let's go back to pre-function component era, where all we needed to care about were the props
. Not so much you needed to do. Choices were PureComponent
and shouldComponentUpdate
. Nothing else. Like this:
https://gist.github.com/07d40c5d6aa0c9a10757afe0670dd453
Right. So if you make such component, this component is only going to update when shouldShowRed
is changed. Otherwise, it is going to stay still even if its parent renders for some reason. It goes the same for shouldComponentUpdate
; It just gives you additional tooling to specify your own method of telling the component when to update.
Again, using the component with redux was pretty straightforward too. Just create a container and pass states and dispatches (I didn't write any mapDispatchToProps
for the sake of simplicity) mapped as props:
https://gist.github.com/6ca97ad910d3b7e7f69895adc8f6720b
Right. Let's assume that we have RootReduxState
as we have seen from the code above. And this is just going to work so well. Nothing difficult here. SomeDiv
will keep being efficient at its best because PureComponent
is working well. But how should we precisely port this example to a function component?
Well, it's easy. Just use useSelector
, Right?
https://gist.github.com/ff02d9e5302c2d0c6bd02875ce7badcf
Perfect. But what if you need more than just shouldShowRed
? The moment you start to get more than one thing from useSelector (which is a very normal case), you are going to need additional optimization efforts with appropriate knowledge.
If we were to use shouldShowGreen
too, we could choose:
Option 1
https://gist.github.com/0497c16c622dcde9f11171e8fd147af5
Or, we could:
Option 2
https://gist.github.com/fb90dbf934e94bff6c17d6a020a0919a
Alternatively:
Option 3
https://gist.github.com/c76f9ca8041fb2ce133d319cc9fb721e
Or, just a small variant..
Option 4
https://gist.github.com/5b4e17388fd4bdf624badf04c2980fcb
Which method have you been using? Whichever one you are using, you should be able to give reasons! Now, before having a look at each option, let's go back to useSelector
and see what it does.
So let me bring the most important thing to the table immediately: useSelector
forces re-render of your component. It's not a prop, but it can still make your component re-render. Really? Yes. Let's look at the offical documentation:
When an action is dispatched, useSelector() will do a reference comparison of the previous selector result value and the current result value. If they are different, the component will be forced to re-render. If they are the same, the component will not re-render.
So how do you judge if they are different? By running strict equality (===
) comparision. (Click here if you are feeling like looking at the official repo's source code). In the source code, useSelector
by default uses a function called refEquality
, and all it does is simply const refEquality = (a, b) => a === b
.
This means that if you are returning an object from useSelector
for whatever reason, useSelector
will cause a re-render every time an action is dispatched from the store, simply because objects of the same structure (same keys and values) do not strictly equal each other in javascript. For example, { a: 1 } === { a: 1 }
is false
. Official doc says the same:
returning a new object every time will always force a re-render by default.
Do they really force it? Yes. From the source code:
https://gist.github.com/b1feeada2bdd8a9aab3da79f1bf8f937
So... now, with this in our mind, let's go back to the options we saw previously.
https://gist.github.com/d8bad6d80265c047255dadcda29171c9
Catches:
- You are just writing a pair of
shouldShowRed
andshouldShowGreen
for three times just to take the desired state out. - This will cause a re-render forcefully every time an action is dispatched, because you are returning a new object from your selector.
Verdict: not good enough.
https://gist.github.com/d089b2cab64e9414b1e2a2533cfa47fe
Catches: you are returning the entire state from your selector, and destructuring it outside of the selector. This will too cause a re-render. What's the point of having the selector callback if you intend to receive the entire state? This is a bad practice. It's just tantamount to passing the entire state in mapStateToProps
. You don't do that there. So, why here?
Verdict: not good enough.
https://gist.github.com/71cb411b06a3a10170d633da2cbe851f
Catches:
-
you are calling
useSelector
twice, and this does not matter. According to the official doc:Because of the React update batching behavior used in React Redux v7, a dispatched action that causes multiple useSelector()s in the same component to return new values should only result in a single re-render.
-
strict equality is functioning as properly for each selected state because you are returning a primitive from each selector.
Verdict: usable.
https://gist.github.com/d271de9483f63b9413de0d842bc607cd
Catches: It's just the same as option 1 or 2. It will cause a re-render too.
Verdict: Not good enough.
Thankfully, redux gives us a chance to insert our own equality functions. The concept is the same shouldComponentUpdate
or the equality callback in React.memo
. We could do this:
https://gist.github.com/af0f1a2984e90754c2b5a903336794b3
or,
https://gist.github.com/b1be7bc0a042e8577df35d31de14ea8c
Something like that. However you should really note that using deepEqual
cannot ever be fast if you are trying to equal a large object (Dan Abramov said it, too!):
Note also that you only want to run deepEqual
on what you need. In the code snippet above, you are also comparing shouldShowBlue
which is a part of RootReduxState['conditions']
. But you don't need that anyways, but you are still comparing it. Make sure you select and compare what you only need.
Well, at first, it's totally okay. Your app has ~100 components only, your redux state is quite shallow, and it does not take a long time to render whatever's being rendered.
The problem comes at two points:
- once you start to scale your application. If you succeeed in making a popular application, you are going to support more features, and thus, need more components. Your components will be numerous, leading to the point where re-render of expensive components will be causing a sluggish interaction.
- once you start to care about users using low-end devices. You want to support users with low-end spec computers. You want to support mobile devies with inherently less performance than most computers. Just get a 6x slowdown on your CPU from Chrome's performance tool and try to see how long it takes for your components to react.
So far we looked at possible problems with using useSelector
and how to solve them:
- If you are returning an object from your selector callback, it's going to force re-render by default.
- To prevent re-render, either let your selector return a primitive type, or use a custom equality function.
That's it. Thank you!