Closed maclockard closed 4 months ago
Something basically equivalent to this was previously added in https://github.com/facebook/react/pull/13139 , and then removed in https://github.com/facebook/react/pull/13861 .
You should also look at https://github.com/facebook/react/pull/20646 and https://github.com/facebook/react/pull/20890 . Not _quite the same thing as what you're proposing, but related.
Thanks for those links! I read through them and there are definitely some interesting ideas. I think in particularly useContextSelector
(#20646) would be helpful for reducing re-renders, but doesn't quite solve the same thing. Ideally useContextGetter
could prevent all re-renders due to context changes, while useContextSelector
still requires some part of the context to be selected on, resulting in a re-render.
Reading through https://github.com/facebook/react/issues/16956 I also find some mention of a request for a hook along the lines of useStateWithGetter
(https://github.com/facebook/react/issues/16956#issuecomment-576583337). This shares a similar motivation to useContextGetter
in terms of reducing how often callback references change.
You can do something like this in your code:
function createContext(defaultValue) {
const Ctx = React.createContext(defaultValue);
const GetterCtx = React.createContext(() => defaultValue);
const BaseProvider = Ctx.Provider;
function Provider({ value, children }) {
const ref = React.useRef(value);
React.useLayoutEffect(() => {
ref.current = value;
});
const getter = React.useCallback(() => ref.current, []);
return <BaseProvider value={value}>
<GetterCtx.Provider value={getter}>
{children}
</GetterCtx.Provider>
</BaseProvider>;
}
Ctx.Provider = Provider;
Ctx.GetterCtx = GetterCtx;
return Ctx;
}
function useContextGetter(ctx) {
return React.useContext(ctx.GetterCtx);
}
Thanks @vkurchatkin I like this approach. Here's a quick demo with your code: https://stackblitz.com/edit/react-contextgetter
@vkurchatkin That's pretty handy and I think would work for most cases, however, reading through this comment about useEventCallback
I think there may be some drawbacks to using useLayoutEffect
for keeping the context value up to date. Specifically if this context ref were to be used by a useLayoutEffect
in child component deeper in tree, the context value would be 'stale' since child effects fire before parent effects.
If this were a part of React, I would hope that there would be some way for the context value to be updated earlier in the lifecycle than useEffect
/useLayoutEffect
making it safe to call in downstream effects.
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!
This is still relevant!
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!
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:Here the value of
MyContext
is only used whenonClick
is called, it is not used by any returned DOM elements or child components. However, if the value ofmyContext
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 preventExpensiveComponent
from re-rendering by returning a getter function forMyContext
that would allowonClick
to lazily access the current context's value. This getter would be a stable function similar to the callbackuseState
returns.Here's the above example rewritten to use
useContextGetter
: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'suseStore
and callinggetState()
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-renderingExpensiveComponent
. 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.