You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Preventing rerenders with React.memo and useContext hook. #15156
I can't rely on data from context API by using (useContext hook) to prevent unnecessary rerenders with React.memo
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:
React.memo(() => {
const [globalState] = useContext(SomeContext);
render ...
}, (prevProps, nextProps) => {
// How to rely on context in here?
// I need to rerender component only if globalState contains nextProps.value
});
What is the expected behavior?
I should have somehow access to the context in React.memo second argument callback to prevent rendering
Or I should have the possibility to return an old instance of the react component in the function body.
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
16.8.4
This is working as designed. There is a longer discussion about this in #14110 if you're curious.
Let's say for some reason you have AppContext whose value has a theme property, and you want to only re-render some ExpensiveTree on appContextValue.theme changes.
TLDR is that for now, you have three options:
Option 1 (Preferred): Split contexts that don't change together
If we just need appContextValue.theme in many components but appContextValue itself changes too often, we could split ThemeContext from AppContext.
functionButton(){lettheme=useContext(ThemeContext);// The rest of your rendering logicreturn<ExpensiveTreeclassName={theme}/>;}
Now any change of AppContext won't re-render ThemeContext consumers.
This is the preferred fix. Then you don't need any special bailout.
Option 2: Split your component in two, put memo in between
If for some reason you can't split out contexts, you can still optimize rendering by splitting a component in two, and passing more specific props to the inner one. You'd still render the outer one, but it should be cheap since it doesn't do anything.
functionButton(){letappContextValue=useContext(AppContext);lettheme=appContextValue.theme;// Your "selector"return<ThemedButtontheme={theme}/>}constThemedButton=memo(({ theme })=>{// The rest of your rendering logicreturn<ExpensiveTreeclassName={theme}/>;});
Option 3: One component with useMemo inside
Finally, we could make our code a bit more verbose but keep it in a single component by wrapping return value in useMemo and specifying its dependencies. Our component would still re-execute, but React wouldn't re-render the child tree if all useMemo inputs are the same.
functionButton(){letappContextValue=useContext(AppContext);lettheme=appContextValue.theme;// Your "selector"returnuseMemo(()=>{// The rest of your rendering logicreturn<ExpensiveTreeclassName={theme}/>;},[theme])}
There might be more solutions in the future but this is what we have now.
Still, note that option 1 is preferable — if some context changes too often, consider splitting it out.
Reacted by nimrossum, Patryk Janik, Narin Luangrath, Nicholas Jamieson, chqfly, louis, TEK, Dony Sukardi, Edward Xiao, Jakub Kopys and 1631 more
Reacted by pasiba, alakhera, Dmitry Zherebko, Denis Orlov, Johnny Peter, Andy, xuqinggang, Eric Lu, Sonu kumar, John A. Kallergis and 45 more
Reacted by Sonu kumar, John A. Kallergis, Ramin Rezaei, lander854, Nathan H. Leung, Petro Pavlenko, Aneree, Peter Styk, iceberg211, Joshua and 40 more
Reacted by Saber Fathallah, Tạ Trường, Diego Matias Pousa, Evgeniy, Ștefan Mihoc, Afshin Hoseini, Rafayel Hovhannisyan, Snake, Ahmed Mahmoud, Jorge Baralt and 79 more
Reacted by jongleb, Andy Lai, Filipp Riabchun, Juliet Adams, Sonu kumar, John A. Kallergis, Hanna, VRG, iceberg211, lannguyen and 30 more
Reacted by Daniel Steigerwald, Alex Sandiiarov, Parmesh Krishen, Gustavo Gard, ChiefORZ, Minh Nguyen, Patryk Karny, Dan Minshew, John Ralph Umandal, Casey Metz and 236 more
Reacted by Josh C, Dan Minshew, Yonnathan Henao, Mark Rozenvain, Arnaud CETOUTE, Davide Gaggero, AG, Vasyl Zubach, Ștefan Mihoc, Gabriel Schmith and 124 more
Reacted by John Ralph Umandal, alakhera, Dimitry, Warren Day, 00050, lsnch, jarick, lin onetwo, Meyyappan Muthuraman, Gabriel Schmith and 103 more
Using the unstable_Profiler option 2 will still trigger onRender callbacks but not call the actual render logic. Maybe I'm doing something wrong? https://codesandbox.io/s/kxz4o2oyoo
Maybe a good solution for that would be to have the possibility of "taking" the context and rerender component only if given callback return true e.g: useContext(ThemeContext, (contextData => contextData.someArray.length !== 0 ));
The main problem with hooks that I actually met is that we can't manage from inside of a hook what is returned by a component - to prevent rendering, return memoized value etc.
Reacted by iamcastelli, Łukasz Pluszczewski, Nguyễn Trọng Vĩnh, Jeff, 范晓, stephen white, Hanna, Jan Hrdý, MinJie, Kamil Lazam and 31 more
Reacted by Aron Gabor, Sergey M., Pouya Jabbarisani, Shyam Mittal, Jason Rice and アンハー
Option 4: Do not use context for data propagation but data subscription. Use useSubscription (because it's hard to write to cover all cases).
Reacted by Thomas Suckow, Ivanov Ivan, straydrop411 - 54yd, Mario Jerkovic, Jakub Švehla, Chris Chen, Arnaud CETOUTE, 范晓, Filipp Riabchun, Junyoung/"Clare" Jang and 7 more
There is another way to avoid re-render.
"You need to move the JSX up a level out of the re-rendering component then it won't get re-created each time"
Maybe a good solution for that would be to have the possibility of "taking" the context and rerender component only if given callback return true e.g: useContext(ThemeContext, (contextData => contextData.someArray.length !== 0 ));
The main problem with hooks that I actually met is that we can't manage from inside of a hook what is returned by a component - to prevent rendering, return memoized value etc.
Instead of a true/false here... could we provide an identity based function that allowed us to subset the data from the context?
where useContext wouldn't pop in this component unless the result of the selector fn was different identity wise from the previous result of the same function.
Reacted by roadwild, Elkin Alberto, Sarfaraz Nawaz and Tran Quang Tien
There is another way to avoid re-render.
"You need to move the JSX up a level out of the re-rendering component then it won't get re-created each time"
@fuleinist Ultimately, it's not that different from MobX, although a lot simplified for a specific use case. MobX already works like that (also using Proxies), the state is mutated and components who use specific bits of the state get re-rendered, nothing else.
Reacted by Drew Hamlett, MinJie, Gaston Morixe, Chris Chen and JimmyLv_吕立青
@gaearon I don't know if I'm missing something, but I have tried yours second and third options and they are not working correctly. Not sure if this is only react chrome extension bug or there is other catch. Here is my simple example of form, where I see rerendering both inputs. In console I see that memo is doing his job but DOM is rerendered all the time. I have tried 1000 items and onChange event is really slow, that's why I think that memo() is not working with context correctly. Thanks for any advice:
Here is demo with 1000 items/textboxes. But in that demo dev tools doesn't show re-render. You have to download sources on local to test it: https://codesandbox.io/embed/zen-firefly-d5bxk
Reacted by Rhoger Anacleto, James Berry, Steve, Ismail Ghallou , Ryan Bower, Dinesh Pandiyan, Hadi El Yakhni, avedetvedea, Matej Husar, Sohel Islam Imran and 16 more
I wonder why the option 3 is not the build-in option for React.
Option 3: One component with useMemo inside
The reason why i say that is, the Root is always bailed out in the single dispatch case. But when the Provider is used, no one is taking care of the path between the Provider to the consumer any more. So if Root can be bailed out always, why not the first children of the Provider. I would relate the Root to the Provider, as in single dispatch vs multiple dispatch case.
Hope this makes sense, because i think Context shouldn't be by default expensive, because it should be designed to solve a very important problem, so if a single dispatch is working, we should just scale the solution up without introducing an "unexpected" behavior from the single case, assuming bailing out the Root is the expected case.
FYI, this change has nothing to do with useContext, maybe i picked the wrong thread, but the solution provided as Option 3 ignites the conversation.
Option 4: Do not use context for data propagation but data subscription. Use useSubscription (because it's hard to write to cover all cases).
What does it mean? Could you explain both data propagation and data subscription using examples? As per my understanding they are same: data is propagated using subscribe/publish, right?
@steida it means that the context value should not be the data, i.e. something that changes, but rather a data source, an object that always stays the same by reference, but has something like subscribe and getCurrentData methods.
@steida it means that the context value should not be the data, i.e. something that changes, but rather a data source, an object that always stays the same by reference, but has something like subscribe and getCurrentData methods.
OK. Such data sources can be global/singleton objects and that would still work? Why do we need context then?
Maybe I am missing something, but @gaearon maybe we should add clarification to Option 1 that it prevents rerenders in some cases; for example here it doesn't help:
import React from 'react';
import './style.css';
const { useState, createContext, useContext, useEffect, useRef } = React;
const ViewContext = createContext();
const ActionsContext = createContext();
function MyContainer() {
const [contextState, setContextState] = useState();
return (
<ViewContext.Provider value={contextState}>
<ActionsContext.Provider value={setContextState}>
<MySetCtxComponent />
<MyViewCtxComponent />
</ActionsContext.Provider>
</ViewContext.Provider>
);
}
function MySetCtxComponent() {
const setContextState = useContext(ActionsContext);
const counter = useRef(0);
console.log('Set');
useEffect(() => {
console.log('=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent');
const intervalID = setInterval(() => {
setContextState('New Value ' + counter.current);
counter.current++;
}, 1000);
return () => clearInterval(intervalID);
}, [setContextState]);
return <button onClick={() => (counter.current = 0)}>Reset</button>;
}
function MyViewCtxComponent() {
const contextState = useContext(ViewContext);
console.log('View');
return <div>This is the value of the context: {contextState}</div>;
}
export default MyContainer;
One can see both "View" and "Set" are being logged, which means both components got rerendered.
Maybe I am missing something, but @gaearon maybe we should add clarification to Option 1 that it prevents rerenders in some cases; for example here it doesn't help:
import React from 'react';
import './style.css';
const { useState, createContext, useContext, useEffect, useRef } = React;
const ViewContext = createContext();
const ActionsContext = createContext();
function MyContainer() {
const [contextState, setContextState] = useState();
return (
<ViewContext.Provider value={contextState}>
<ActionsContext.Provider value={setContextState}>
<MySetCtxComponent />
<MyViewCtxComponent />
</ActionsContext.Provider>
</ViewContext.Provider>
);
}
function MySetCtxComponent() {
const setContextState = useContext(ActionsContext);
const counter = useRef(0);
console.log('Set');
useEffect(() => {
console.log('=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent');
const intervalID = setInterval(() => {
setContextState('New Value ' + counter.current);
counter.current++;
}, 1000);
return () => clearInterval(intervalID);
}, [setContextState]);
return <button onClick={() => (counter.current = 0)}>Reset</button>;
}
function MyViewCtxComponent() {
const contextState = useContext(ViewContext);
console.log('View');
return <div>This is the value of the context: {contextState}</div>;
}
export default MyContainer;
One can see both "View" and "Set" are being logged, which means both components got rerendered.
Wondering the same, have you found any solution for this specific example?
Maybe I am missing something, but @gaearon maybe we should add clarification to Option 1 that it prevents rerenders in some cases; for example here it doesn't help:
import React from 'react';
import './style.css';
const { useState, createContext, useContext, useEffect, useRef } = React;
const ViewContext = createContext();
const ActionsContext = createContext();
function MyContainer() {
const [contextState, setContextState] = useState();
return (
<ViewContext.Provider value={contextState}>
<ActionsContext.Provider value={setContextState}>
<MySetCtxComponent />
<MyViewCtxComponent />
</ActionsContext.Provider>
</ViewContext.Provider>
);
}
function MySetCtxComponent() {
const setContextState = useContext(ActionsContext);
const counter = useRef(0);
console.log('Set');
useEffect(() => {
console.log('=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent');
const intervalID = setInterval(() => {
setContextState('New Value ' + counter.current);
counter.current++;
}, 1000);
return () => clearInterval(intervalID);
}, [setContextState]);
return <button onClick={() => (counter.current = 0)}>Reset</button>;
}
function MyViewCtxComponent() {
const contextState = useContext(ViewContext);
console.log('View');
return <div>This is the value of the context: {contextState}</div>;
}
export default MyContainer;
One can see both "View" and "Set" are being logged, which means both components got rerendered.
Wondering the same, have you found any solution for this specific example?
@hrvojegolcic
There is one approach, some people do:
Now if you change contextState of Wrapper, it will not re-render the children which don't rely on the context. Because react does some optimization (more info): since props.children didn't change (it comes from parent which didn't re-render), it will skip re-rendering it even though Wrapper re-rendered. It will re-render only those children, that rely on context which changed.
@gmoniava This is worth to take into consideration, I'll check thanks. But your example from the above is very specific, one component uses only the GET state and another only the SET state, but even with the SET state, it's still the same context and it will indeed behave like both need to re-render. React will understand SET state, as using the context, while it's not really a consumer that needs to re-render.
@gmoniava This is worth to take into consideration, I'll check thanks. But your example from the above is very specific, one component uses only the GET state and another only the SET state, but even with the SET state, it's still the same context and it will indeed behave like both need to re-render. React will understand SET state, as using the context, while it's not really a consumer that needs to re-render.
@hrvojegolcic
I think why both components re-render in my case is that we updated state at the top parent (contextState), this by default causes all children to re-render, this is standard react behavior and the context providers in between don't prevent any re-renders, that would be a job of memo, if it were in between somewhere.
@gmoniava I think I got ya, and it helps. Taking that into consideration then it seems the useMemo/useCallback will do here. As in your example, from my understanding the line <ActionsContext.Provider value={setContextState}> could change to <ActionsContext.Provider value={useCallback(setContextState, [])}>. That way, the setContextState will not report as changed when contextState is changed.
@hrvojegolcic No, in my example the components MySetCtxComponent and MyViewCtxComponent re-rendered not because setContextState changed (it didn't). But because I re-rendered the parent MyContainer which caused its children to re-render by default (this is standard react behavior). So in this case, splitting the context doesn't save any re-renders, I suppose gaearon above was talking about different use case. Probably he was having something like this in mind:
let MySetCtxComponent = React.memo(() => {
const setContextState = useContext(ActionsContext);
const counter = useRef(0);
console.log('MySetCtxComponent');
useEffect(() => {
const intervalID = setInterval(() => {
setContextState('New Value ' + counter.current);
counter.current++;
}, 1000);
return () => clearInterval(intervalID);
}, [setContextState]);
return <button onClick={() => (counter.current = 0)}>Reset</button>;
});
let MyViewCtxComponent = React.memo(() => {
const contextState = useContext(ViewContext);
console.log('MyViewCtxComponent');
return <div>This is the value of the context: {contextState}</div>;
});
I just wrapped the two components in memo. Now, if you run the original code with these examples, you can see only MyViewCtxComponent re-renders, because the context value which it uses changed.
As you can see it's very easy to use and optimal. The key point is to create as many slices of Context as you need, and this library makes it extremely easy and straightforward to do it.
Activity
gaearon commentedon Mar 19, 2019
This is working as designed. There is a longer discussion about this in #14110 if you're curious.
Let's say for some reason you have
AppContextwhose value has athemeproperty, and you want to only re-render someExpensiveTreeonappContextValue.themechanges.TLDR is that for now, you have three options:
Option 1 (Preferred): Split contexts that don't change together
If we just need
appContextValue.themein many components butappContextValueitself changes too often, we could splitThemeContextfromAppContext.Now any change of
AppContextwon't re-renderThemeContextconsumers.This is the preferred fix. Then you don't need any special bailout.
Option 2: Split your component in two, put
memoin betweenIf for some reason you can't split out contexts, you can still optimize rendering by splitting a component in two, and passing more specific props to the inner one. You'd still render the outer one, but it should be cheap since it doesn't do anything.
Option 3: One component with
useMemoinsideFinally, we could make our code a bit more verbose but keep it in a single component by wrapping return value in
useMemoand specifying its dependencies. Our component would still re-execute, but React wouldn't re-render the child tree if alluseMemoinputs are the same.There might be more solutions in the future but this is what we have now.
Still, note that option 1 is preferable — if some context changes too often, consider splitting it out.
eps1lon commentedon Mar 19, 2019
@gaearon Are the Buttons the children or do the Buttons render children? I'm missing some context how these are used.
Using the
unstable_Profileroption 2 will still triggeronRendercallbacks but not call the actual render logic. Maybe I'm doing something wrong?https://codesandbox.io/s/kxz4o2oyoohttps://codesandbox.io/s/00yn9yqzjwgaearon commentedon Mar 20, 2019
I updated the example to be clearer.
gaearon commentedon Mar 20, 2019
That's exactly the point of that option. :-)
pumanitro commentedon Mar 20, 2019
Maybe a good solution for that would be to have the possibility of "taking" the context and rerender component only if given callback return true e.g:
useContext(ThemeContext, (contextData => contextData.someArray.length !== 0 ));The main problem with hooks that I actually met is that we can't manage from inside of a hook what is returned by a component - to prevent rendering, return memoized value etc.
gaearon commentedon Mar 20, 2019
If we could, it wouldn't be composable.
https://overreacted.io/why-isnt-x-a-hook/#not-a-hook-usebailout
steida commentedon Apr 2, 2019
Option 4: Do not use context for data propagation but data subscription. Use useSubscription (because it's hard to write to cover all cases).
some simple examples of preventing re-renders due to store changes
Alfrex92 commentedon Jun 25, 2019
There is another way to avoid re-render.
"You need to move the JSX up a level out of the re-rendering component then it won't get re-created each time"
More info here
jonnolen commentedon Jul 11, 2019
Instead of a true/false here... could we provide an identity based function that allowed us to subset the data from the context?
const contextDataINeed = useContext(ContextObj, (state) => state['keyICareAbout'])where useContext wouldn't pop in this component unless the result of the selector fn was different identity wise from the previous result of the same function.
fuleinist commentedon Jul 12, 2019
found this library that it may be the solution for Facebook to integrate with hooks https://blog.axlight.com/posts/super-performant-global-state-with-react-context-and-hooks/
fuleinist commentedon Jul 12, 2019
Problem is it may be costly to restructure the components tree just to prevent top to bottom re-rendering.
danielkcz commentedon Jul 13, 2019
@fuleinist Ultimately, it's not that different from MobX, although a lot simplified for a specific use case. MobX already works like that (also using Proxies), the state is mutated and components who use specific bits of the state get re-rendered, nothing else.
marrkeri commentedon Aug 7, 2019
@gaearon I don't know if I'm missing something, but I have tried yours second and third options and they are not working correctly. Not sure if this is only react chrome extension bug or there is other catch. Here is my simple example of form, where I see rerendering both inputs. In console I see that memo is doing his job but DOM is rerendered all the time. I have tried 1000 items and onChange event is really slow, that's why I think that memo() is not working with context correctly. Thanks for any advice:
Here is demo with 1000 items/textboxes. But in that demo dev tools doesn't show re-render. You have to download sources on local to test it: https://codesandbox.io/embed/zen-firefly-d5bxk
On the other hand this approach without context works correctly, still in debug it is slower than I expected but at least rerender is ok
31 remaining items
rajan-keypress commentedon Jul 24, 2021
Better Solution: Bigger Application use Redux
mugwhump commentedon Aug 15, 2021
For anyone reading this thread now, the context selector functionality was removed from the linked library and put in a standalone library.
windmaomao commentedon Aug 29, 2021
I wonder why the option 3 is not the build-in option for React.
The reason why i say that is, the Root is always bailed out in the single dispatch case. But when the Provider is used, no one is taking care of the path between the Provider to the consumer any more. So if Root can be bailed out always, why not the first children of the Provider. I would relate the Root to the Provider, as in single dispatch vs multiple dispatch case.
Maybe we can have a component called
ContextProviderSimplewhich joins theContextProviderandSimpleMemo. I blogged this approach here, https://windmaomao.medium.com/should-a-context-provider-be-that-expensive-7cafa3727507.Hope this makes sense, because i think Context shouldn't be by default expensive, because it should be designed to solve a very important problem, so if a single dispatch is working, we should just scale the solution up without introducing an "unexpected" behavior from the single case, assuming bailing out the Root is the expected case.
FYI, this change has nothing to do with
useContext, maybe i picked the wrong thread, but the solution provided as Option 3 ignites the conversation.snawaz commentedon Jul 17, 2022
@steida
What does it mean? Could you explain both data propagation and data subscription using examples? As per my understanding they are same: data is propagated using subscribe/publish, right?
Hypnosphi commentedon Jul 18, 2022
@steida it means that the context value should not be the data, i.e. something that changes, but rather a data source, an object that always stays the same by reference, but has something like
subscribeandgetCurrentDatamethods.snawaz commentedon Jul 18, 2022
@Hypnosphi
OK. Such data sources can be global/singleton objects and that would still work? Why do we need context then?
gmoniava commentedon Feb 19, 2023
Maybe I am missing something, but @gaearon maybe we should add clarification to Option 1 that it prevents rerenders in some cases; for example here it doesn't help:
One can see both "View" and "Set" are being logged, which means both components got rerendered.
hrvojegolcic commentedon May 3, 2023
Wondering the same, have you found any solution for this specific example?
gmoniava commentedon May 3, 2023
@hrvojegolcic
There is one approach, some people do:
Now if you change
contextStateofWrapper, it will not re-render the children which don't rely on the context. Because react does some optimization (more info): sinceprops.childrendidn't change (it comes from parent which didn't re-render), it will skip re-rendering it even thoughWrapperre-rendered. It will re-render only those children, that rely on context which changed.hrvojegolcic commentedon May 5, 2023
@gmoniava This is worth to take into consideration, I'll check thanks. But your example from the above is very specific, one component uses only the GET state and another only the SET state, but even with the SET state, it's still the same context and it will indeed behave like both need to re-render. React will understand SET state, as using the context, while it's not really a consumer that needs to re-render.
gmoniava commentedon May 5, 2023
@hrvojegolcic
I think why both components re-render in my case is that we updated state at the top parent (
contextState), this by default causes all children to re-render, this is standard react behavior and the context providers in between don't prevent any re-renders, that would be a job of memo, if it were in between somewhere.hrvojegolcic commentedon May 6, 2023
@gmoniava I think I got ya, and it helps. Taking that into consideration then it seems the
useMemo/useCallbackwill do here. As in your example, from my understanding the line<ActionsContext.Provider value={setContextState}>could change to<ActionsContext.Provider value={useCallback(setContextState, [])}>. That way, thesetContextStatewill not report as changed whencontextStateis changed.gmoniava commentedon May 6, 2023
@hrvojegolcic No, in my example the components
MySetCtxComponentandMyViewCtxComponentre-rendered not becausesetContextStatechanged (it didn't). But because I re-rendered the parentMyContainerwhich caused its children to re-render by default (this is standard react behavior). So in this case, splitting the context doesn't save any re-renders, I suppose gaearon above was talking about different use case. Probably he was having something like this in mind:I just wrapped the two components in memo. Now, if you run the original code with these examples, you can see only
MyViewCtxComponentre-renders, because the context value which it uses changed.roggc commentedon Jun 12, 2023
Take a look at the library react-context-slices. With this library you do it like this:
As you can see it's very easy to use and optimal. The key point is to create as many slices of Context as you need, and this library makes it extremely easy and straightforward to do it.
smitkh commentedon Sep 19, 2024
If you're experiencing unnecessary re-renders when using the Context API, switching to the
useSyncExternalStorehook could solve your issue.This hook allows you to manage state in a more efficient way, preventing components from re-rendering when they don't need to.
I recently wrote a blog on this topic, explaining how it works and why it's a great alternative to the Context API for global state management.
Check it out here:
https://medium.com/@smit-khanpara/enhance-react-performance-replace-context-api-with-usesyncexternalstore-for-better-state-6af420cf7951