I have a simple question that has to do with React rendering. Here's the link to the code sandbox before I explain: =/src/App.js
Here's the deal, I have an array of objects stored in a parent ponent called App. Each object has a 'checked' field which I want to toggle on clicking on that object's respective checkbox. I loop through the array of objects and display them each within a Child ponent. When I click on a checkbox, the handleChange function executes and that list is updated within the parent ponent, causing App to rerender along with ALL of the Child ponents. The problem I want to solve is, how can I make it so I only rerender the Child ponent that was clicked instead of all of them?
I tried using useCallback along with a functional update to the list state, but that didn't do anything and it still rerenders all of the child ponents instead of the one being toggled. I have a hunch that I'm using useCallback incorrectly, and that a brand new function is being created. I'd like an explanation of how React does it's rerendering when it es to arrays, paring the previous array against the new array. I understand that in my code I'm providing a copy of the original list by destructuring it and then putting it inside a new array, which obviously is not a reference to the original list so React sets the copy as the new state:
App.js
import { useCallback, useState } from "react";
import Child from "./Child";
import "./styles.css";
const mockList = [
{ text: "1", id: 1, checked: false },
{ text: "2", id: 2, checked: false },
{ text: "3", id: 3, checked: false },
{ text: "4", id: 4, checked: false },
{ text: "5", id: 5, checked: false }
];
export default function App() {
const [list, setList] = useState(mockList);
const handleChange = useCallback((checked, id) => {
setList((oldList) => {
for (let i = 0; i < oldList.length; i++) {
if (oldList[i].id === id) {
oldList[i].checked = checked;
break;
}
}
return [...oldList];
});
}, []);
return (
<div className="App">
{list.map((item) => (
<Child
key={item.id}
text={item.text}
checked={item.checked}
handleChange={(checked) => handleChange(checked, item.id)}
/>
))}
</div>
);
}
Child.js
const Child = ({ text, checked, handleChange }) => {
console.log("Child rerender");
return (
<div
style={{
display: "flex",
border: "1px solid green",
justifyContent: "space-around"
}}
>
<p>{text}</p>
<input
style={{ width: "20rem" }}
type="checkbox"
checked={checked}
onChange={(e) => handleChange(e.checked)}
/>
</div>
);
};
export default Child;
I have a simple question that has to do with React rendering. Here's the link to the code sandbox before I explain: https://codesandbox.io/s/list-rerendering-y3iust?file=/src/App.js
Here's the deal, I have an array of objects stored in a parent ponent called App. Each object has a 'checked' field which I want to toggle on clicking on that object's respective checkbox. I loop through the array of objects and display them each within a Child ponent. When I click on a checkbox, the handleChange function executes and that list is updated within the parent ponent, causing App to rerender along with ALL of the Child ponents. The problem I want to solve is, how can I make it so I only rerender the Child ponent that was clicked instead of all of them?
I tried using useCallback along with a functional update to the list state, but that didn't do anything and it still rerenders all of the child ponents instead of the one being toggled. I have a hunch that I'm using useCallback incorrectly, and that a brand new function is being created. I'd like an explanation of how React does it's rerendering when it es to arrays, paring the previous array against the new array. I understand that in my code I'm providing a copy of the original list by destructuring it and then putting it inside a new array, which obviously is not a reference to the original list so React sets the copy as the new state:
App.js
import { useCallback, useState } from "react";
import Child from "./Child";
import "./styles.css";
const mockList = [
{ text: "1", id: 1, checked: false },
{ text: "2", id: 2, checked: false },
{ text: "3", id: 3, checked: false },
{ text: "4", id: 4, checked: false },
{ text: "5", id: 5, checked: false }
];
export default function App() {
const [list, setList] = useState(mockList);
const handleChange = useCallback((checked, id) => {
setList((oldList) => {
for (let i = 0; i < oldList.length; i++) {
if (oldList[i].id === id) {
oldList[i].checked = checked;
break;
}
}
return [...oldList];
});
}, []);
return (
<div className="App">
{list.map((item) => (
<Child
key={item.id}
text={item.text}
checked={item.checked}
handleChange={(checked) => handleChange(checked, item.id)}
/>
))}
</div>
);
}
Child.js
const Child = ({ text, checked, handleChange }) => {
console.log("Child rerender");
return (
<div
style={{
display: "flex",
border: "1px solid green",
justifyContent: "space-around"
}}
>
<p>{text}</p>
<input
style={{ width: "20rem" }}
type="checkbox"
checked={checked}
onChange={(e) => handleChange(e.checked)}
/>
</div>
);
};
export default Child;
Share
Improve this question
asked Aug 6, 2022 at 3:11
AdrianRonquilloAdrianRonquillo
1191 gold badge3 silver badges7 bronze badges
1
-
You can make the
Child
ponent a Pure Component usingReact.memo()
to wrap the ponent where you export it. – Irfanullah Jan Commented Aug 6, 2022 at 3:20
2 Answers
Reset to default 3Here's how you optimize it, first you use useCallback
wrong, because every rerender the (e) => handleChange(e.checked)
is a new instance, hence even if we memo the Child
it will still rerender because props is always new.
So we need to useCallback to the function that invoke handleChange
see my forked codesandbox
https://codesandbox.io/s/list-rerendering-forked-tlkwgh?file=/src/App.js
React has nothing to do with how you manipulate your arrays or objects. It simply goes on rendering your app tree and there are certain rules that decide when to stop rerendering a certain branch within the tree. Rendering simply means React calls the ponent functions which themselves return a sub-tree or nodes. Please see https://reactjs/docs/reconciliation.html
Try wrapping the Child
with React.memo():
const Child = ({ text, checked, handleChange }) => {
console.log("Child rerender");
return (
<div
style={{
display: "flex",
border: "1px solid green",
justifyContent: "space-around"
}}
>
<p>{text}</p>
<input
style={{ width: "20rem" }}
type="checkbox"
checked={checked}
onChange={(e) => handleChange(e.checked)}
/>
</div>
);
};
export default React.memo(Child);
What this React.memo()
does is essentially pare previous props with next props and only rerenders the Child
ponent if any of the props have changed and doesn't re-render if the props have stayed the same.
As a side note: Please read this https://kentcdodds./blog/usememo-and-usecallback
TLDR: Over optimisation at the cost of code plexity is not remended. And preventing “unnecessary” renders probably will have a drawback in the form of extra memory usage and putation: so only need to do it if very sure it will help in a specific problem.