React To Many Re Renders In Context [duplicate] - reactjs

Whenever I update user (object) in users (array) in context - all components which uses users re-renders.
What I've tried
I have a component which is using values from context:
const DashboardCardList=()=> {
const context = useContext(StaticsContext);
const users = context.users.filter(user=>user.visible);
return !users
? <Loading/>
: (
<Container>
{users.map(user=>
<DashboardCard key={user._id} user={user}/>
)}
</Container>
);
};
My update function (updates context state):
const onUserUpdate=(user)=>{
const index = this.state.users.findIndex(item => item._id === user._id);
const users = [...this.state.users]
users[index] = user;
this.setState({users:users});
}
Final component:
const DashboardCard=({user})=> {
console.log("I'm here!", user);
return ...;
}
Question
Why it keeps re-rendering? Is it because of context?
How to write this properly?

There is no render bailout for context consumers (v17).
Here is a demonstration, where the Consumer will always re-render just because he is a Context Consumer, even though he doesn't consume anything.
import React, { useState, useContext, useMemo } from "react";
import ReactDOM from "react-dom";
// People wonder why the component gets render although the used value didn't change
const Context = React.createContext();
const Provider = ({ children }) => {
const [counter, setCounter] = useState(0);
const value = useMemo(() => {
const count = () => setCounter(p => p + 1);
return [counter, count];
}, [counter]);
return <Context.Provider value={value}>{children}</Context.Provider>;
};
const Consumer = React.memo(() => {
useContext(Context);
console.log("rendered");
return <>Consumer</>;
});
const ContextChanger = () => {
const [, count] = useContext(Context);
return <button onClick={count}>Count</button>;
};
const App = () => {
return (
<Provider>
<Consumer />
<ContextChanger />
</Provider>
);
};
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
To fix it:
Use a single context for each consumed value. Meaning that context holds a single value, (and no, there is no problem with multiple contexts in an application).
Use a state management solution like Recoil.js, Redux, MobX, etc. (although it might be overkill, think good about app design beforehand).
Minor optimization can be achieved by memoizing Provider's values with useMemo.

Related

How To Make All Consumers of The Context Able to Change Context State but Only Some of Them Will be Re-rendered [duplicate]

Whenever I update user (object) in users (array) in context - all components which uses users re-renders.
What I've tried
I have a component which is using values from context:
const DashboardCardList=()=> {
const context = useContext(StaticsContext);
const users = context.users.filter(user=>user.visible);
return !users
? <Loading/>
: (
<Container>
{users.map(user=>
<DashboardCard key={user._id} user={user}/>
)}
</Container>
);
};
My update function (updates context state):
const onUserUpdate=(user)=>{
const index = this.state.users.findIndex(item => item._id === user._id);
const users = [...this.state.users]
users[index] = user;
this.setState({users:users});
}
Final component:
const DashboardCard=({user})=> {
console.log("I'm here!", user);
return ...;
}
Question
Why it keeps re-rendering? Is it because of context?
How to write this properly?
There is no render bailout for context consumers (v17).
Here is a demonstration, where the Consumer will always re-render just because he is a Context Consumer, even though he doesn't consume anything.
import React, { useState, useContext, useMemo } from "react";
import ReactDOM from "react-dom";
// People wonder why the component gets render although the used value didn't change
const Context = React.createContext();
const Provider = ({ children }) => {
const [counter, setCounter] = useState(0);
const value = useMemo(() => {
const count = () => setCounter(p => p + 1);
return [counter, count];
}, [counter]);
return <Context.Provider value={value}>{children}</Context.Provider>;
};
const Consumer = React.memo(() => {
useContext(Context);
console.log("rendered");
return <>Consumer</>;
});
const ContextChanger = () => {
const [, count] = useContext(Context);
return <button onClick={count}>Count</button>;
};
const App = () => {
return (
<Provider>
<Consumer />
<ContextChanger />
</Provider>
);
};
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
To fix it:
Use a single context for each consumed value. Meaning that context holds a single value, (and no, there is no problem with multiple contexts in an application).
Use a state management solution like Recoil.js, Redux, MobX, etc. (although it might be overkill, think good about app design beforehand).
Minor optimization can be achieved by memoizing Provider's values with useMemo.

Avoid runnning an effect hook when Context get updated

I have a component MyContainer which has a state variable (defined via useState hook), defines a context provider to which it passes the state variable as value and contains also 2 children, MySetCtxComponent and MyViewCtxComponent.
MySetCtxComponent can change the value stored in the context invoking a set function which is also passed as part of the context, BUT DOES NOT RENDER it.
MyViewCtxComponent, on the contrary, RENDERS the value stored in the context.
MySetCtxComponent defines also an effect via useEffect hook. This effect is, for instance, used to update the value of the context at a fixed interval of time.
So the code of the 3 components is this
MyContainer
export function MyContainer() {
const [myContextValue, setMyContextValue] = useState<string>(null);
const setCtxVal = (newVal: string) => {
setMyContextValue(newVal);
};
return (
<MyContext.Provider
value={{ value: myContextValue, setMyContextValue: setCtxVal }}
>
<MySetCtxComponent />
<MyViewCtxComponent />
</MyContext.Provider>
);
}
MySetCtxComponent
(plus a global varibale to make the example simpler)
let counter = 0;
export function MySetCtxComponent() {
const myCtx = useContext(MyContext);
useEffect(() => {
console.log("=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent");
const intervalID = setInterval(() => {
myCtx.setMyContextValue("New Value " + counter);
counter++;
}, 3000);
return () => clearInterval(intervalID);
}, [myCtx]);
return <button onClick={() => (counter = 0)}>Reset</button>;
}
MyViewCtxComponent
export function MyViewCtxComponent() {
const myCtx = useContext(MyContext);
return (
<div>
This is the value of the contex: {myCtx.value}
</div>
);
}
Now my problem is that, in this way, everytime the context is updated the effect of MySetCtxComponent is run again even if this is not at all required since MySetCtxComponent does not need to render when the context is updated. But, if I remove myCtx from the dependency array of the useEffect hook (which prevents the effect hook when the context get updated), then I get an es-lint warning such as React Hook useEffect has a missing dependency: 'myCtx'. Either include it or remove the dependency array react-hooks/exhaustive-deps.
Finally the question: is this a case where it is safe to ignore the warning or do I have a fundamental design error here and maybe should opt to use a store? Consider that the example may look pretty silly, but it is the most stripped down version of a real scenario.
Here a stackblitz to replicate the case
One pattern for solving this is to split the context in two, providing one context for actions and another for accessing the context value. This allows you to fulfill the expected dependency array of the useEffect correctly, while also not running it unnecessarily when only the context value has changed.
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);
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);
return (
<div>
This is the value of the context: {contextState}
</div>
);
}
ReactDOM.render(
<MyContainer />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
The problem is what you're passing to the useEffect dependency array in MySetCtxComponent. You should only pass the update function as shown below.
However, personally I would destructure out the setter as it's more readable and naturally avoids this issue.
const { useState, createContext, useContext, useEffect, useRef, useCallback } = React;
const MyContext = createContext();
function MyContainer() {
const [myContextValue, setMyContextValue] = useState(null);
// this function is currently unnecessary, but left in because I assume you change the functions default behvaiour in your real code
// also this should be wrapped in a useCallback if used
const setCtxVal = useCallback((newVal: string) => {
setMyContextValue(newVal);
}, [setMyContextValue]);
return (
<MyContext.Provider value={{ value: myContextValue, setMyContextValue: setCtxVal }}>
<MySetCtxComponent />
<MyViewCtxComponent />
</MyContext.Provider>
)
}
function MySetCtxComponent() {
const myCtx = useContext(MyContext);
// or const { setMyContextValue } = useContext(MyContext);
const counter = useRef(0);
useEffect(() => {
console.log("=======>>>>>>>>>>>> Use Effect run in MySetCtxComponent");
const intervalID = setInterval(() => {
myCtx.setMyContextValue("New Value " + counter.current);
// or setMyContextValue("New Value " + counter.current);
counter.current++;
}, 1000);
return () => clearInterval(intervalID);
}, [myCtx.setMyContextValue, /* or setMyContextValue */]);
return <button onClick={() => (counter.current = 0)}>Reset</button>;
}
function MyViewCtxComponent() {
const myCtx = useContext(MyContext);
return (
<div>
This is the value of the context: {myCtx.value}
</div>
);
}
ReactDOM.render(
<MyContainer />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Prevent context.consumer from re-rendering component

I have created the following context provider. In sort it's a toast generator. It can have multiple toasts visible at the same time.
It all worked great and such until I realized that the <Component/> further down the tree that called the const context = useContext(ToastContext) aka the consumer of this context and the creator of the toast notifications, was also re-rendering when the providerValue was changing.
I tried to prevent that, changing the useMemo to a useState hook for the providerValue, which did stop my re-rendering problem , but now I could only have 1 toast active at a time (because toasts was never updated inside the add function).
Is there a way to have both my scenarios?
export const withToastProvider = (Component) => {
const WithToastProvider = (props) => {
const [toasts, setToasts] = useState([])
const add = (toastSettings) => {
const id = generateUEID()
setToasts([...toasts, { id, toastSettings }])
}
const remove = (id) => setToasts(toasts.filter((t) => t.id !== id))
// const [providerValue] = useState({ add, remove })
const providerValue = React.useMemo(() => {
return { add, remove }
}, [toasts])
const renderToasts = toasts.map((t, index) => (
<ToastNote key={t.id} remove={() => remove(t.id)} {...t.toastSettings} />
))
return (
<ToastContext.Provider value={providerValue}>
<Component {...props} />
<ToastWrapper>{renderToasts}</ToastWrapper>
</ToastContext.Provider>
)
}
return WithToastProvider
}
Thank you #cbdeveloper, I figured it out.
The problem was not on my Context but on the caller. I needed to use a useMemo() there to have memoized the part of the component that didnt need to update.

Create a hook with a consumer (provider/consumer)

How can I create a hook based on a consumer and provider?
The reason for why I am asking this is because the Provider and Consumer is exported from another library, so I can not extract that part more than that, and I would like a neat way to just add a value to the useHook to populate wanted values.
// I have a provider somewhere above this part, and here is the consumer:
<MyConsumer>
{props => <Values {...props} />}
</MyConsumer>
I would like to create a hook that returns the values I get inside the Values component. And maybe the best is to include the MyConsumer inside that hook so I would not need "two lines of code" where I want to use the hook.
I am imagining something like:
const useValues = (props) => {
const [state, setState] = useState({});
// do stuff
return state;
}
As mention, I do not know what I should do about the MyConsumer part.
Thank you for any help
You can do this by using your own context to wrap the provider and consumer.
const MyStateCtx = createContext();
const useMyState = () => useContext(MyStateCtx);
// LibraryProvider and LibraryConsumer are the provider and consumer
// supplied by the library you're using.
const MyStateProvider = ({ children }) => {
return (
<LibraryProvider>
<LibraryConsumer>
{(props) => (
<MyStateCtx.Provider value={props}>{children}</MyStateCtx.Provider>
)}
</LibraryConsumer>
</LibraryProvider>
);
};
const ConsumingComponent = () => {
// You can consume state here with this hook ✨
const myState = useMyState();
return <div />;
};
export default function App() {
return (
<MyStateProvider>
<div>
<ConsumingComponent />
</div>
</MyStateProvider>
);
}
Codesandbox link

What's the difference between useObserver and observer in this application?

I have a react functional component that accesses the MobX store with useContext. I have found two ways to observe an array that is an observable from the store. First, the useObserver hook and wrapping the component with observer.
I thought that these are the same but that the useObserver only observes specific properties (such as the array that is passed) but I am experiencing a problem when the array reaches size 2 and then the component does not re-render. That's the case when using useObserver. When wrapping with observer, this is fixed.
Can anyone explain why this is happening and what's the difference?
const ApplesContainer = observer(() => {
const stores = useStores();
const applesArray = stores.fruits.apples;
return (
{applesArray.map(apple => (
<Apple key={apple.id} apple={apple} />
))}
);
});
// OR with useObserver()
function useGlobalState() {
const stores = useStores();
return useObserver(() => ({
applesArray: stores.fruits.apples
}));
}
const ApplesContainer = observer(() => {
const { applesArray } = useGlobalState();
return (
{applesArray.map(apple => (
<Apple key={apple.id} apple={apple} />
))}
);
});
useObserver must return JSX with an observable value.
This hook takes care of tracking changes and re-rendering them.
If no observable value exists in JSX, then it won't be re-rendered.
e.g.:
const SomeContainer =() => {
const { someStores } = useStores();
return useObserver(()=>(
{someStore.data.map(val => (
<Apple key={val.id} val={val} />
))}
));
};

Resources