最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - React - How to rerender only one component from an array of objects that is changing - Stack Overflow

programmeradmin1浏览0评论

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 using React.memo() to wrap the ponent where you export it. – Irfanullah Jan Commented Aug 6, 2022 at 3:20
Add a ment  | 

2 Answers 2

Reset to default 3

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

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.

发布评论

评论列表(0)

  1. 暂无评论