Reputation: 3451
I have a simple example where I pass a clickFunction
as a value to React Context and then access that value in a child component. That child component re-renders event though I'm using React.memo and React.useCallback. I have an example in stackblitz that does not have the re-render problem without using context here:
https://stackblitz.com/edit/react-y5w2cp (no problem with this)
But, when I add context and pass the the function as part of the value of the context, all children component re-render. Example showing problem here:
https://stackblitz.com/edit/react-wpnmuk
Here is the problem code:
Hello.js
import React, { useCallback, useState, createContext } from "react";
import Speaker from "./Speaker";
export const GlobalContext = createContext({});
export default () => {
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
const [speakers, setSpeakers] = useState(speakersArray);
const clickFunction = useCallback((speakerIdClicked) => {
setSpeakers((currentState) =>
currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
})
);
}, []);
return (
<GlobalContext.Provider
value={{
clickFunction: memoizedValue,
}}
>
{speakers.map((rec) => {
return <Speaker speaker={rec} key={rec.id}></Speaker>;
})}
</GlobalContext.Provider>
);
};
Speaker.js
import React, {useContext} from "react";
import { GlobalContext } from "./Hello";
export default React.memo(({ speaker }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
const { clickFunction } = useContext(GlobalContext);
return (
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
</button>
);
});
WORKING CODE BELOW FROM ANSWERS BELOW
Speaker.js
import React, { useContext } from "react";
import { GlobalContext } from "./Hello";
export default React.memo(({ speaker }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
const { clickFunction } = useContext(GlobalContext);
return (
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
</button>
);
});
Hello.js
import React, { useState, createContext, useMemo } from "react";
import Speaker from "./Speaker";
export const GlobalContext = createContext({});
export default () => {
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
const [speakers, setSpeakers] = useState(speakersArray);
const clickFunction = (speakerIdClicked) => {
setSpeakers((currentState) =>
currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
})
);
};
const provider = useMemo(() => {
return ({clickFunction: clickFunction});
}, []);
return (
<GlobalContext.Provider value={provider}>
{speakers.map((rec) => {
return <Speaker speaker={rec} key={rec.id}></Speaker>;
})}
</GlobalContext.Provider>
);
};
Upvotes: 0
Views: 1336
Reputation: 17608
This is because your value
you pass to your provider changes every time. So, this causes a re-render because your Speaker
component thinks the value
is changed.
Maybe you can use something like this:
const memoizedValue = useMemo(() => ({ clickFunction }), []);
and remove useCallback
from the function definition since useMemo
will handle this part for you.
const clickFunction = speakerIdClicked =>
setSpeakers(currentState =>
currentState.map(rec => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
})
);
and pass this to your provider such as:
<GlobalContext.Provider value={memoizedValue}>
<Speaker speaker={rec} key={rec.id} />
</GlobalContext.Provider>
After providing the answer, I've realized that you are using Context
somehow wrong. You are mapping an array and creating multiple providers for each data. You should probably change your logic.
Update:
Most of the time you want to keep the state in your context. So, you can get it from the value
as well. Providing a working example below. Be careful about the function this time, we are using useCallback
for it to get a stable reference.
const GlobalContext = React.createContext({});
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
function App() {
const [speakers, setSpeakers] = React.useState(speakersArray);
const clickFunction = React.useCallback((speakerIdClicked) => {
setSpeakers((currentState) =>
currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
})
);
}, []);
const memoizedValue = React.useMemo(() => ({ speakers, clickFunction }), [
speakers,
clickFunction,
]);
return (
<GlobalContext.Provider value={memoizedValue}>
<Speakers />
</GlobalContext.Provider>
);
}
function Speakers() {
const { speakers, clickFunction } = React.useContext(GlobalContext);
return speakers.map((speaker) => (
<Speaker key={speaker.id} speaker={speaker} clickFunction={clickFunction} />
));
}
const Speaker = React.memo(({ speaker, clickFunction }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
return (
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id} {speaker.favorite === true ? "true" : "false"}
</button>
);
});
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root" />
Upvotes: 2
Reputation: 3507
when passing value={{clickFunction}} as prop to Provider like this when the component re render and will recreate this object so which will make child update, so to prevent this you need to memoized the value with useMemo.
here the code:
import React, { useCallback, useState, createContext,useMemo } from "react";
import Speaker from "./Speaker";
export const GlobalContext = createContext({});
export default () => {
const speakersArray = [
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
];
const [speakers, setSpeakers] = useState(speakersArray);
const clickFunction = useCallback((speakerIdClicked) => {
setSpeakers((currentState) =>
currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
})
);
}, []);
const provider =useMemo(()=>({clickFunction}),[])
return (
<div>
{speakers.map((rec) => {
return (
<GlobalContext.Provider value={provider}>
<Speaker
speaker={rec}
key={rec.id}
></Speaker>
</GlobalContext.Provider>
);
})}
</div>
);
};
note you dont need to use useCallback
anymore clickFunction
Upvotes: 3