I've seen a few questions related to this topic, but none that tackle the issue head-on in a pure way. useContext is a great tool for minimizing prop-drilling and centralizing key data needed across your app; however, it comes at a cost that I'm trying to minimize.
The closest issue to the one I'm describing here was asked two years ago here. The question didn't gain a lot of traction and the only answer basically says call the context in a parent container and pass values down through props (defeats the purpose of context) or use Redux. Maybe that's the only way, but I wanted to bring the question back to the collective to see if there is a better answer that's not dependent on external libraries.
I've set up a code sandbox here to illustrate the issue. Re-renders are console logged out to make seeing them easier.
In short, when a state changes in context, every app accessing data from that context re-renders, even if it's not utilizing the state data that changed (because it's all ultimately passed through the value object). React.Memo does not work on values accessed from context the same way it does for properties.
For example, in the code sandbox linked above, in App.js if you comment out <AppContainerWithContext />
and load <AppContainer />
, there are two states managed in the container, one called titleText
and the other called paragraphText
. titleText
is passed as a prop to component called TitleText
and paragraphText
is passed to a component called ParagraphText
. Both components are wrapped in React.memo()
. There are two buttons called in the AppContainer
and each has a function that changes the text back and forth based on the value of separate boolean states.
Here is the function that toggles the titleText, the one for paragraph text is the same logic:
const changeTitleHandler = useCallback(() => {
const title = listTitleToggle ? "Title B" : "Title A";
setListTitleToggle((pV) => !pV)
setTitleText(title);
}, [listTitleToggle]);
Since the titleText
component and paragraphText
components are wrapped with React.useMemo
, they only re-render when the corresponding value passed to them changes. Perfect.
Now in App.js
if you comment out the <AppContainer />
component and enable the <AppContainerWithContext />
component, the rendered output and result of button clicks is identical; however, the states that change and are rendered on the screen are now managed by AppContext (called contextTitleText
and contextParagraphText
and passed to the TitleText
component and ParagraphText
component via useContext.
Now, if you click on the button to toggle the title, the ParagraphText
component re-renders too, even though it doesn't use the contextTitleText
state. My understanding of why this happens is because the value object changes when the contextTitleText is updated, causing any component accessing that value object through useContext to re-render.
My question is this:
Is there a way to utilize useContext without causing re-renders on all components accessing the context. In the example above, can we utilize useContext to manage the contextTitleText
and the contextParagraphText
but only re-render the components where the state from context being accessed changes?