Home > Back-end >  React - How to rerender only one component from an array of objects that is changing
React - How to rerender only one component from an array of objects that is changing

Time:08-06

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

  • Related