Closed pumanitro closed 5 years ago
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:
function Wrapper(props) {
const [contextState, setContextState] = useState();
return (
<ActionsContext.Provider value={{ contextState, setContextState }}>
{props.children}
</ActionsContext.Provider>
);
}
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.
Take a look at the library react-context-slices. With this library you do it like this:
// slices.js
import getHookAndProviderFromSlices from "react-context-slices";
export const {useSlice, Provider} = getHookAndProviderFromSlices({
count1: {initialArg: 0},
count2: {initialArg: 0},
// rest of slices
});
// app.js
import {useSlice} from "./slices"
const App = () => {
const [count1, setCount1] = useSlice("count1");
const [count2, setCount2] = useSlice("count2");
return <>
<div>
<button onClick={()=>setCount1(c => c + 1)}>+</button>{count1}
</div>
<div>
<button onClick={()=>setCount2(c => c + 1)}>+</button>{count2}
</div>
</>;
};
export default App;
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.
If you're experiencing unnecessary re-renders when using the Context API, switching to the useSyncExternalStore
hook 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
Do you want to request a feature or report a bug?
bug
What is the current behavior?
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:
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