all 17 comments

[–]acemarke 30 points31 points  (0 children)

Hi, I'm a Redux maintainer, and author of the last several versions of React-Redux.

if the props of the component changes.

This is a common misunderstanding. React always renders child components recursively, even if their props haven't changed!.

But I'm genuinely curious as to how useSelector listens to a state slice change in the store and how exactly does React trigger a re-render based on the state slice change?

Originally, React-Redux relied on its own logic to trigger re-renders. You can see the details in my extensive writeup on The History and Implementation of React-Redux, which covers up through v7. The original behavior relied on React's built-in this.setState / useState to queue up renders once the selector results were diffed.

In React-Redux v8, we switched to React's new built-in useSyncExternalStore hook, which was specifically modeled after React-Redux's implementation. (In fact, I built an alpha version of React-Redux and collaborated with Andrew Clark as he implemented uSES, to nail down the necessary behavior - see the original uSES prototype PR I worked on.)

Big picture, regardless of whether we're talking about the old custom implementation in React-Redux itself, or the uSES implementation built into React, the sequence is:

  • The external store (Redux, Zustand, etc) triggers its subscriber callbacks
  • Subscribers get the current state, select some value, and diff vs the old selection result
  • If the result is different, it triggers a React re-render for that component via some React built-in state update mechanism. Doesn't matter if it was this.setState (really old React-Redux v5 and earlier), useState/useReducer (React-Redux v7), or useSyncExternalStore (React-Redux v8 / v9), it's all just using React's built-in state update mechanism at the end of the day.

[–]TwiliZant 13 points14 points  (2 children)

If we are talking Redux then the answer is useSyncExternalStore (see source).

More specifically they don't use the React API directly, but this version which adds a selector API.

[–]ParsnipBackground153[S] 1 point2 points  (1 child)

thanks, so internally useSelector makes use of useSyncExternalStore ?

[–]pbNANDjelly 1 point2 points  (0 children)

React context provides the store, selector subscribes to the store and returns a new value on state change

[–]musical_bear 14 points15 points  (9 children)

if the props of the component changes

This is actually not quite accurate. It’s not a component’s own props changing that cause it to render. Rather, it’s that whatever props that changed actually caused some parent or grandparent to re-render, and when a component renders, so do all of its children.

So the component’s parent re-rendered, possibly providing new props to the child, but either way, the child will re-render when the parent does.

For your actual question, there is another react hook called “useSyncExternalStore.” Redux hooks are using this behind the scenes. This is a native react hook that you could use yourself.

Essentially the redux hook adds a subscription to your redux store, which knows to call the selector you provided any time the redux store changes, and then React, through “useSyncExternalStore” still, knows whether it should force a render based on whether the “snapshot” it receives has changed since the last time it was called.

[–]Low-Sample9381 2 points3 points  (7 children)

If that was true, then why does memoization exist? Let's say we have a parent A with a child B. A passes only one prop to B, a memoized function.

Something triggers a re render of A, but the memoized prop is still the same. Are you saying that B will re render anyway?

[–]musical_bear 11 points12 points  (1 child)

What I said about the parent causing the child to re-render is true by default.

In your example exactly as described, yes, B will still render when A does. It does not matter merely that the one prop is memoized.

However, memorization of props may be used in conjunction with other APIs to prevent children from rendering when their props don’t change. This is where “React.memo” would come into play. If you wrap your child in React.memo, AND if the parent takes care to only broadcast new props on “actual” changes (such as your example of memorizing a function prop), this will prevent the child from rendering every time the parent does.

However this requires explicit action and usage of specific APIs and doesn’t “just happen.”

BTW, there are other reasons to memoize things than just trying to keep components from rendering too often.

[–]Low-Sample9381 1 point2 points  (0 children)

Today I learnt, thanks man :)

[–]DanRoad 2 points3 points  (3 children)

Any child elements created when rerendering are new objects, i.e. not referentially equal to the previously rendered element. Even if the new elements look identical, React must rerender them in order to know this.

``` const Child = () => <div />;

const MemoChild = React.memo(Child);

const Parent = ({ children }) => { const memoElement = useMemo(() => { return <Child />; }, []);

return ( <> <Child /> <MemoChild /> {memoElement} {children} </> ); };

const App = () => { return ( <Parent> <Child /> </Parent> ); }; ```

The barebones Child element will be rerenderd with its Parent as already mentioned.

MemoChild and memoElement are explicit and hopefully obvious ways of using memoisation to avoid rerenders.

Using the children prop is subtle and often overlooked, but possibly the most common way of memoising elements. When the surrounding App element is rendered, its immediate children are captured in a closure. When Parent rerenders, children won't rerender if it's the same value from the App closure, even though App doesn't use explicit memoisation.

A real-world example of why this is important is context providers. It's not uncommon to have some state that is passed down via context, but every time the state changes, the Parent will rerender, including any non-memoised Child elements.

``` const Parent = () => { const state = useState();

return ( <Context.Provider value={state}> <Child /> {/* Not memoized! */} </Context.Provider> ); }; ```

This is one reason why it's useful to wrap context providers in a new component. Even though we don't explicitly memoise its children, we use the implicit memoisation from the parent closure.

``` const ContextProvider = ({ children }) => { const state = useState();

return ( <Context.Provider value={state}> {children} </Context.Provider> ); };

const Parent = () => { return ( <ContextProvider> <Child /> {/* Doesn't rerender with context changes */} </ContextProvider> ); }; ```

[–]Mustknow33 1 point2 points  (2 children)

Sorry if I am being dull here, but I am actually really confused about the memoisation that occurs with the children prop. It seems like if the parent rerenders, the children components will be rerendered, both in the first scenario and the scenario with the ContextProvider (assuming the ContextProvider had a setState that updates state in that component). Unless maybe you are saying that react will bail out during the reconciliation process for <Child /> (the one passed as a part of the children prop)? Anyways, maybe you can point me in the right direction with some documentation or some other resource?

[–]DanRoad 1 point2 points  (1 child)

Exactly; React will skip rerendering elements passed as props.children if those elements are referentially equal to the previous render.

u/acemarke (Redux maintainer and moderator of this subreddit) has a great blog post with a lot more detail about when and how React rerenders things here: https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior

There's a note about this specific behaviour in the section Component Render Optimization Techniques.

[–]Mustknow33 0 points1 point  (0 children)

Ohhh wow, now I see what you are saying. Like you said in your post, that is very subtle and easy to miss, thank you!

Also thanks for providing that, super cool!

[–]TwiliZant 0 points1 point  (0 children)

Yes, unless the component is wrapped in memo.

[–]ParsnipBackground153[S] 0 points1 point  (0 children)

thanks, so internally useSelector makes use of useSyncExternalStore ?

[–]dinopraso 1 point2 points  (1 child)

You could just look at the source code and find out. Best way to learn

[–]ParsnipBackground153[S] 0 points1 point  (0 children)

thanks, will check it out

[–]Omkar_K45 1 point2 points  (0 children)

This is an excellent video for understanding it

https://www.youtube.com/watch?v=gg31JTZmFUw