Reputation: 9742
When researching contexts I've stumbled across a common pattern more than once:
const storeContext = React.createContext()
const Provider = (children) => {
const [state, setState] = React.useState();
const ctxValue = React.useMemo(() => [state, setState], [state]);
return (
<storeContext.Provider value={ctxValue}>
{children}
</storeContext.Provider>
)
}
Many people seem to use useMemo
in this case, and I'm not sure why. At first glance, it seems to make sense since passing an object/array directly would mean that this object would get recreated on every render and trigger the children to be rerendered since a new reference is passed. At second glance, I struggle with the pattern.
I'm not sure how this pattern could potentially prevent any rerenders. If a component up the tree changes, all children (including the context) rerender anyway, and if state
changes, the provider rerenders (rightfully) itself as well as all children.
The only thing that I see useMemo
do is to save a tiny bit of memory since we're not recreating the ctxValue
object on every render.
I'm not sure if I'm missing something here, and would love some input.
Upvotes: 2
Views: 660
Reputation: 4024
The reason is because every render would create a new Array instance. By using useMemo, the array passed to the context is only ever changed when state changes. A better way to do this would be to just pass the result of useState to the context:
const storeContext = React.createContext()
const Provider = (children) => {
const ctxValue = React.useState();
return (
<storeContext.Provider value={ctxValue}>
{children}
</storeContext.Provider>
)
}
So, I was pretty curious as to how React would handle re-renders and contexts updating. I was really surprised to find that React will only re-render a component if the props change.
For example, if you have something like this:
function MyComponent(){
const ctxValue = useContext(MyContext);
return (<div>
value: {ctxValue}
<MyOtherComponent />
</div>);
}
And you render that component, MyOtherComponent
will only render once, ever, assuming the component doesn't use any hooks that can update its internal state.
The same goes for Context.Provider. The only time it will re-render is when the value updates, but its children will not re-render unless they directly depend on that context.
I create a simple sandbox to demonstrate this.
Basically, I set up a lot of different ways to nest children, and some children rely on the context, and others don't. If you click the "+" button, the context is updated and you'll see the render count only increase for those components.
const Ctx = React.createContext();
function useRenderCount(){
const count = React.useRef(0);
count.current += 1;
return count.current;
}
function wrapInRenderCount(name,child){
const count = useRenderCount();
return (
<div>
<div>
{name} was rendered {count} times.
</div>
<div style={{ marginLeft: "1em" }}>{child}</div>
</div>
);
}
function ContextProvider(props) {
const [count, setState] = React.useState(0);
const increase = React.useCallback(() => {
setState((v) => v + 1);
}, []);
const context = React.useMemo(() => [increase, count], [increase, count]);
return wrapInRenderCount('ContextProvider',
<Ctx.Provider value={context}>
{props.children}
</Ctx.Provider>
);
}
function ContextUser(props){
const [increase, count] = React.useContext(Ctx);
return wrapInRenderCount('ContextUser',
<div>
<div>
Count: {count}
<button onClick={increase}>
++
</button>
</div>
<div>
{props.children}
</div>
</div>
);
}
function RandomWrapper(props){
return wrapInRenderCount('RandomWrapper',<div>
<div>
Wrapped
</div>
<div style={{marginLeft:'1em'}}>
{props.children}
</div>
</div>);
}
function RandomContextUserWrapper(props){
return wrapInRenderCount('RandomContextUserWrapper',
<div>
<ContextUser></ContextUser>
{props.children}
</div>
)
}
function App() {
return wrapInRenderCount('App',
<RandomWrapper>
<ContextProvider>
<RandomWrapper>
<ContextUser>
<RandomWrapper>
<RandomContextUserWrapper>
<RandomWrapper>
<ContextUser/>
</RandomWrapper>
</RandomContextUserWrapper>
</RandomWrapper>
</ContextUser>
<RandomWrapper>
<RandomContextUserWrapper />
</RandomWrapper>
</RandomWrapper>
</ContextProvider>
</RandomWrapper>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Upvotes: 2