Description
Right now the only hook for consuming a React context is useContext
, which is great for most cases. However, one downside is that it results in a component re-rendering whether or not the context itself is directly used for displaying something. Take the following example:
export const ExpensiveComponent = React.memo(function ExpensiveComponent() {
const myContext = useContext(MyContext);
const onClick = useCallback(() => {
doThing(myContext);
}, [myContext]);
// lots of other hooks
return (
<div>
<button onClick={onClick}>Click Me</button>
{/* ...other children... */}
</div>
);
});
Here the value of MyContext
is only used when onClick
is called, it is not used by any returned DOM elements or child components. However, if the value of myContext
changes, ExpensiveComponent
will re-render despite no differences in what is being displayed.
One way to prevent this component from over re-rendering would be to provide a hook along the lines of useContextGetter
. It would prevent ExpensiveComponent
from re-rendering by returning a getter function for MyContext
that would allow onClick
to lazily access the current context's value. This getter would be a stable function similar to the callback useState
returns.
Here's the above example rewritten to use useContextGetter
:
export const ExpensiveComponent = React.memo(function ExpensiveComponent() {
const getMyContext = useContextGetter(MyContext);
const onClick = useCallback(() => {
doThing(getMyContext());
}, [getMyContext]);
// lots of other hooks
return (
<div>
<button onClick={onClick}>Click Me</button>
{/* ...other children... */}
</div>
);
});
There is some prior art for an API similar to this with Recoil's useRecoilCallback
making it possible to access Recoil state inside of a callback without requiring a component to re-render when the state changes. One could also construct similar functionality with React Redux's useStore
and calling getState()
on the store inside of a callback.
The above examples I used are pretty trivial and one could simply refactor the part that uses MyContext
into a separate child component to avoid re-rendering ExpensiveComponent
. However, its not difficult to imagine a scenario where such a refactor may be challenging or a component being used in enough places that the re-render causes performance degradation.