React Memo 重置组件状态中的值

React Memo resets values in component state

我想做什么

  1. 我正在尝试使用一组 objects(习惯)并从每个习惯中渲染一个“卡片”组件。
  2. 现在,对于每张卡片(习惯​​),您都可以将该习惯“标记”为已完成,这将更新该卡片的状态。
  3. 我已经使用 React.memo 来防止其他卡 re-rendering。
  4. 只要用户将卡片标记为完成,卡片 header 就会更改为“已编辑”

我面临的问题

每当一张卡片被标记为完成时,header 会正常改变,但是每当任何 other 卡片被标记为完成时,第一张卡片的状态就会恢复。

我找不到其他面临类似问题的人,有人可以帮忙吗?

代码如下:

import React, { useState } from "react";

const initialState = {
  habits: [
    {
      id: "1615649099565",
      name: "Reading",
      description: "",
      startDate: "2021-03-13",
      doneTasksOn: ["2021-03-13"]
    },
    {
      id: "1615649107911",
      name: "Workout",
      description: "",
      startDate: "2021-03-13",
      doneTasksOn: ["2021-03-14"]
    },
    {
      id: "1615649401885",
      name: "Swimming",
      description: "",
      startDate: "2021-03-13",
      doneTasksOn: []
    },
    {
      id: "1615702630514",
      name: "Arts",
      description: "",
      startDate: "2021-03-14",
      doneTasksOn: ["2021-03-14"]
    }
  ]
};

export default function App() {
  const [habits, setHabits] = useState(initialState.habits);

  const markHabitDone = (id) => {
    let newHabits = [...habits];
    let habitToEditIdx = undefined;

    for (let i = 0; i < newHabits.length; i++) {
      if (newHabits[i].id === id) {
        habitToEditIdx = i;
        break;
      }
    }

    let habit = { ...habits[habitToEditIdx], doneTasksOn: [], name: "Edited" };
    newHabits[habitToEditIdx] = habit;
    setHabits(newHabits);
  };

  return (
    <div className="App">
      <section className="test-habit-cards-container">
        {habits.map((habit) => {
          return (
            <MemoizedCard
              markHabitDone={markHabitDone}
              key={habit.id}
              {...habit}
            />
          );
        })}
      </section>
    </div>
  );
}

const Card = ({
  id,
  name,
  description,
  startDate,
  doneTasksOn,
  markHabitDone
}) => {
  console.log(`Rendering ${name}`);
  return (
    <section className="test-card">
      <h2>{name}</h2>
      <small>{description}</small>
      <h3>{startDate}</h3>
      <small>{doneTasksOn}</small>
      <div>
        <button onClick={() => markHabitDone(id, name)}>Mark Done</button>
      </div>
    </section>
  );
};

const areCardEqual = (prevProps, nextProps) => {
  const matched =
    prevProps.id === nextProps.id &&
    prevProps.doneTasksOn === nextProps.doneTasksOn;

  return matched;
};

const MemoizedCard = React.memo(Card, areCardEqual);

注意:无需在 Card 组件上使用 React.memo() 包装即可正常工作。

这里是codesandbox link: https://codesandbox.io/s/winter-water-c2592?file=/src/App.js

问题是因为您的(自定义)记忆 markHabitDone 在某些组件中变成了陈旧的闭包。

注意如何将 markHabitDone 传递给组件。现在假设您单击其中一张卡片并将其标记为完成。由于您的自定义记忆功能,其他卡片将不会被重新渲染,因此它们仍然会有一个 markHabitDone 来自先前渲染的实例.因此,当您现在更新新卡片中的项目时:

let newHabits = [...habits];

...habits 来自之前的渲染。所以旧项目基本上都是这样重新创建的。

memo 中使用自定义函数进行比较,就像您的 areCardEqual 函数一样,可能会很棘手,因为您可能会忘记比较某些道具并留下陈旧的闭包。

其中一个解决方案是取消 memo 中的自定义比较函数,并考虑将 useCallback 用于 markHabitDone 函数。如果您还对 useCallback 使用 [],那么您必须重写 markHabitDone 函数(使用 setStatefunctional 形式),这样它就不会读取habits 使用闭包,就像您在该函数的第一行中那样(否则它将始终读取 habits 的旧值,因为 useCallback 中的数组为空)。