How useSelector can trigger an update only when we want it to

Daniel Merrill
Async
Published in
5 min readFeb 7, 2020

--

A deep dive into the inner workings of a seemingly-impossible hook

When a react context updates, all components that use that context also update. This would cause huge performance issues if all components with react-redux’s useSelector re-rendered each time any part of the redux store changed. So how does useSelector work?

I am a big fan of React’s context api, but performance issues can crop up when two pieces of state live in a context and one of them updates often while the other does not. If I only need the less-frequently-changing piece of state in my component, it will still re-render every time the more-frequently-changing piece of state updates.

The useSelector hook from react-redux doesn’t have this issue — components only re-render when their selected piece of state changes, even when other slices of the store are updated. So how does it work? Does it somehow bypass the rules of context?

Of course, the answer is “no”, but useSelector employs some creative tactics to get there. Before we begin, let’s take a closer look at the problem. A contrived example of a context with two pieces of state, clicks and time, might look like this:

One piece of state (time) tracks the number of seconds elapsed since opening the app, another (clicks) tracks how many times the user clicks a button. Here’s how we might use them in our app:

Run on codesandbox: https://codesandbox.io/s/naive-context-woidu

The timer increments every second, and the click counter increments whenever our button is clicked. However, since the click component reads from the same context as the timer, it also re-renders every time the timer increments, even when the click state hasn’t changed. Not good!

The accepted solution seems to be “split your state into separate contexts”, but this doesn’t feel great to me as it creates artificial boundaries. If I’m creating an audio player, I don’t want to have to split up my isPlaying state from currentTime just because currentTime changes often and isPlaying does not.

Now that we understand the problem, let’s look at useSelector. If I were to build react-redux from scratch, it might look something like this.

Full example on codesandbox: https://codesandbox.io/s/dark-sky-jul54

While my useSelector correctly returns the state of the store, it suffers from the same problem as above and re-renders <Clicker /> every time the timer updates. So how does the useSelector from react-redux get around this?

I’m not an author or expert on react/redux, but after digging around the source code, I think I have the answer: the react-redux context value never actually changes.

Let me clarify that. Redux has an ever-changing store, but the way we access the state of the store is through its getState method. The reference to the getState method does not change. The store state returned from getState() may change, but the getter method itself remains referentially equal throughout the app’s lifecycle.

This would normally be a no-no in react-land — we usually want our layouts to declaratively re-render when the state they rely on changes. However, as we just saw, this can also cause unwanted renders.

So how does a component “know" when to re-render if it can’t rely on changes to its props/context? A different pattern is used: a subscription.

useSelector registers a subscriber that gets called whenever the redux store gets updated, and then if that update results in a change to the selected state, it triggers a re-render and returns the new value. That subscription happens here:

subscription.onStateChange = checkForUpdates

See where this happens in the react-redux code on GitHub

The subscription calls checkForUpdates, which checks whether the update to the store resulted in a change to the selected state. If state has changed, a re-render is triggered by calling forceRender({}):

See where this happens in the react-redux code on GitHub

The only job that forceRender has is, unsurprisingly, to force a re-render. It does this by incrementing a piece of local state in a sorta-hacky-but-simple way — every time forceRender is called, it increments an internal counter, which isn’t actually used for anything, but has the desired effect:

const [, forceRender] = useReducer(s => s + 1, 0)

See where this happens in the react-redux code on GitHub

This re-render in turn causes useSelector to select the appropriate piece of state from the store, which is then returned to our component:

selectedState = selector(store.getState())...return selectedState

See where this happens in the react-redux code on GitHub

To summarize, a subscription is set up that fires whenever any redux state changes. When the subscription fires, it calls a function within each instance of useSelector that checks whether its internal reference to selected (old) store state is equal to the selection using the current (new) state of the store. If they differ, a new render is forced, returning the updated state. This doesn’t feel very “reacty” to me (whatever that means), but it does the trick!*

You may see a gotcha here. If my selector returns multiple combined pieces of state, the newly-created selected object will never be referentially equal to the last time the subscription fired, which will put us back in the same boat where re-renders occur too often.

useSelector(state => {
// Each time this runs it returns a brand new object
return {
thingOne: state.thingOne
thingTwo: state.thingTwo
}
})

This can be mitigated by using an equality function like shallowEqual as the second argument to useSelector. See https://react-redux.js.org/next/api/hooks#equality-comparisons-and-updates

Just for good measure, here’s a working-but-minimal useSelector hook created from scratch that only triggers an update when its selected state changes:

https://codesandbox.io/s/peaceful-river-dj5ps
NOTE: While rolling your own useSelector is an interesting exercise, I am NOT advocating that you do this in a production app!

Please let me know if this was helpful to you or if I missed anything!

*The reason the subscriber pattern doesn’t feel “reacty” to me is that the component is no longer just the result of “props and state”. Instead, the parent context has to hold on to function references from within each child instance of useSelector that are explicitly called at the right time.

**I’ve been using Kent C. Dodds’ context pattern and am liking it a lot — it doesn’t help with the issue discussed above but makes working with context a lot friendlier: https://kentcdodds.com/blog/how-to-use-react-context-effectively

--

--

Async
Async

Published in Async

Modern Engineering for Growing Companies

Responses (6)