Blogged Answers: React, Redux, and Context Behavior

This is a post in the Blogged Answers series.

An explanation of how React Context behaves, and how React-Redux uses Context internally

There's a couple assumptions that I've seen pop up repeatedly:

  • React-Redux is "just a wrapper around React context"
  • You can avoid re-renders caused by React context if you destructure the context value

Both of these assumptions are incorrect, and I want to clarify how they actually work so that you can avoid mis-using them in the future.

For context behavior, say we have this initial setup:

function ProviderComponent() {
    const [contextValue, setContextValue] = useState({a: 1, b: 2});

    return (
        <MyContext.Provider value={contextValue}>
            <SomeLargeComponentTree />

function ChildComponent() {
    const {a} = useContext(MyContext);
    return <div>{a}</div>

If the ProviderComponent were to then call setContextValue({a: 1, b: 3}), the ChildComponent would re-render, even though it only cares about the a field based on destructuring. It also doesn't matter how many levels of hooks are wrapping that useContext(MyContext) call. A new reference was passed into the provider, so all consumers will re-render. In fact, if I were to explicitly re-render with <MyContext.Provider value={{a: 1, b: 2}}>, ChildComponent would still re-render because a new object reference has been passed into the provider! (Note that this is why you should never pass object literals directly into context providers, but rather either keep the data in state or memoize the creation of the context value.)

For React-Redux: yes, it uses context internally, but only to pass the Redux store instance down to child components - it doesn't pass the store state using context!. If you look at the actual implementation, it's roughly this but with more complexity:

function useSelector(selector) {
    const [, forceRender] = useReducer( counter => counter + 1, 0);
    const {store} = useContext(ReactReduxContext);
    const selectedValueRef = useRef(selector(store.getState()));

    useLayoutEffect(() => {
        const unsubscribe = store.subscribe(() => {
            const storeState = store.getState();
            const latestSelectedValue = selector(storeState);

            if(latestSelectedValue !== selectedValueRef.current) {
                 selectedValueRef.current = latestSelectedValue;
        return unsubscribe;
    }, [store])

    return selectedValueRef.current;

So, React-Redux only uses context to pass the store itself down, and then uses store.subscribe() to be notified when the store state has changed. This results in very different performance behavior than using context to pass data.

There was an extensive discussion of context behavior in React issue #14110: Provide more ways to bail out of hooks. In that thread, Sebastian Markbage specifically said:

My personal summary is that new context is ready to be used for low frequency unlikely updates (like locale/theme). It's also good to use it in the same way as old context was used. I.e. for static values and then propagate updates through subscriptions. It's not ready to be used as a replacement for all Flux-like state propagation.

In fact, we did try to pass the store state in context in React-Redux v6, and it turned out to be insufficiently performant for our needs, which is why we had to rewrite the internal implementation to use direct subscriptions again in React-Redux v7.

For complete detail on how React-Redux actually works, read my post The History and Implementation of React-Redux, which covers the changes to the internal implementation over time, and how we actually use context.

This is a post in the Blogged Answers series. Other posts in this series: