Blogged Answers: React, Redux, and Context Behavior
This is a post in the Blogged Answers series.
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 />
</MyContext.Provider>
)
}
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;
forceRender();
}
})
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:
- Aug 08, 2023 - Blogged Answers: My Experience Modernizing Packages to ESM
- Jul 06, 2022 - Blogged Answers: How I Estimate NPM Package Market Share (and how Redux usage compares to other libraries)
- Jun 22, 2021 - Blogged Answers: The Evolution of Redux Testing Approaches
- Jan 18, 2021 - Blogged Answers: Why React Context is Not a "State Management" Tool (and Why It Doesn't Replace Redux)
- Jun 21, 2020 - Blogged Answers: React Components, Reusability, and Abstraction
- May 17, 2020 - Blogged Answers: A (Mostly) Complete Guide to React Rendering Behavior
- May 12, 2020 - Blogged Answers: Why I Write
- Feb 22, 2020 - Blogged Answers: Why Redux Toolkit Uses Thunks for Async Logic
- Feb 22, 2020 - Blogged Answers: Coder vs Tech Lead - Balancing Roles
- Jan 19, 2020 - Blogged Answers: React, Redux, and Context Behavior
- Jan 01, 2020 - Blogged Answers: Years in Review, 2018-2019
- Jan 01, 2020 - Blogged Answers: Reasons to Use Thunks
- Jan 01, 2020 - Blogged Answers: A Comparison of Redux Batching Techniques
- Nov 26, 2019 - Blogged Answers: Learning and Using TypeScript as an App Dev and a Library Maintainer
- Jul 10, 2019 - Blogged Answers: Thoughts on React Hooks, Redux, and Separation of Concerns
- Jan 19, 2019 - Blogged Answers: Debugging Tips
- Mar 29, 2018 - Blogged Answers: Redux - Not Dead Yet!
- Dec 18, 2017 - Blogged Answers: Resources for Learning Redux
- Dec 18, 2017 - Blogged Answers: Resources for Learning React
- Aug 02, 2017 - Blogged Answers: Webpack HMR vs React-Hot-Loader
- Sep 14, 2016 - How I Got Here: My Journey Into the World of Redux and Open Source