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 component 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 component. When I click on a checkbox, the handleChange function executes and that list is updated within the parent component, causing App to rerender along with ALL of the Child components. The problem I want to solve is, how can I make it so I only rerender the Child component 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 components 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 comes to arrays, comparing 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;
CodePudding user response:
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 component functions which themselves return a sub-tree or nodes. Please see https://reactjs.org/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 compare previous props with next props and only rerenders the Child
component if any of the props have changed and doesn't re-render if the props have stayed the same.
CodePudding user response:
Here'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