React 组件 props 在应该改变的时候没有改变?

React component props not changing when they should?

我正在从事的一个项目涉及一个大型组件树,向下传递来自顶部组件中大型复杂状态对象的道具。

在一个特定的组件“ChecklistEditor”中,我将“subchecklists”道具映射到“Subchecklist”组件中,每个组件都有各自的道具。我发现的问题是,当我更新子清单的状态(在父组件中)时,它会导致 ChecklistEditor 的道具(子清单)发生变化(正如我从 devtools 中看到的那样),但即使这些道具被传递给子清单组件map 函数,Subchecklist 组件的 props 不会改变,也不会使用更新后的数据重新渲染。

ChecklistEditor.js

import { useState, useCallback } from "react";

import ChecklistTitle from "../ChecklistTitle/ChecklistTitle";
import Subchecklist from "../Subchecklist/Subchecklist";
import NewSubchecklistForm from "../NewSubchecklistForm/NewSubchecklistForm";
import BlankSpace from "../BlankSpace/BlankSpace";
import Button from "../../UI/Button";
import classes from "./ChecklistEditor.module.css";
import useMemoizedCallback from "../../../hooks/useMemoizedCallback";
import SectionTitle from "../SectionTitle/SectionTitle";

const ChecklistEditor = (props) => {
  const [dragItemIndex, setDragItemIndex] = useState(-1);
  const [draggedOverItemIndex, setDraggedOverItemIndex] = useState(-1);

  // Called when the subchecklist is dragged.
  const handleDrag = useCallback(
    (subchecklistIndex) => {
      setDragItemIndex(subchecklistIndex);
    },
    [setDragItemIndex]
  );

  // Called when another subchecklist is dragged over this subchecklist.
  const handleDragOver = useCallback(
    (itemIndex) => {
      setDraggedOverItemIndex(itemIndex);
    },
    [setDraggedOverItemIndex]
  );

  // Called when the dragend event fires for a subchecklist. (Memoized so that it doesnt cause tons of re-renders).
  const handleDragEnd = useMemoizedCallback(() => {
    props.onReorderSubchecklists(dragItemIndex, draggedOverItemIndex);
  }, [props.onReorderSubchecklists, dragItemIndex, draggedOverItemIndex]);

  // Resets the index of the subchecklist being dragged.
  const resetDragItemIndex = useCallback(() => {
    setDragItemIndex(-1);
  }, [setDragItemIndex]);

  // Map item data into Subchecklist elements
  const subchecklists = props.subchecklists.map((item, index) => {
    let returnItem;
    if (item.type === "subchecklist") {
      returnItem = (
        <Subchecklist
          key={item.id}
          subchecklistIndex={index}
          subchecklistId={item.id}
          title={item.title}
          color={item.color}
          items={item.checkItems}
          onDeleteSubchecklist={props.onDeleteSubchecklist}
          onUpdateSubchecklistTitle={props.onUpdateSubchecklistTitle}
          onChangeColor={props.onChangeSubchecklistColor}
          onItemUpdate={props.onItemUpdate}
          onDeleteItem={props.onDeleteItem}
          onAddCheckItem={props.onAddCheckItem}
          onAddCondition={props.onAddCondition}
          onDrag={handleDrag}
          onDragOver={handleDragOver}
          onDragEnd={handleDragEnd}
          onReorderCheckItems={props.onReorderCheckItems}
          resetDragItemIndex={resetDragItemIndex}
        />
      );
    } else if (item.type === "blankSpace") {
      returnItem = (
        <BlankSpace
          key={item.id}
          id={item.id}
          index={index}
          height={item.height}
          onUpdateHeight={props.onUpdateBlankSpace}
          onDelete={props.onDeleteSubchecklist}
          onDrag={handleDrag}
          onDragOver={handleDragOver}
          onDragEnd={handleDragEnd}
          onReorderCheckItems={props.onReorderCheckItems}
          resetDragItemIndex={resetDragItemIndex}
        />
      );
    } else if (item.type === "sectionTitle") {
      returnItem = (
        <SectionTitle
          key={item.id}
          id={item.id}
          index={index}
          title={item.title}
          color={item.color}
          onUpdateTitle={props.onUpdateSectionTitle}
          onChangeColor={props.onChangeSubchecklistColor}
          onDelete={props.onDeleteSubchecklist}
          onDrag={handleDrag}
          onDragOver={handleDragOver}
          onDragEnd={handleDragEnd}
          onReorderCheckItems={props.onReorderCheckItems}
          resetDragItemIndex={resetDragItemIndex}
        />
      );
    }
    return returnItem;
  });

  return (
    <div className={classes.container}>
      <div className={classes.editor}>
        <ChecklistTitle
          title={props.name}
          onTitleChange={props.onTitleChange}
        />
        <Button type="submit" onClick={props.onSave}>
          Save
        </Button>
        <div className={classes.subchecklistContainer}>{subchecklists}</div>
        <NewSubchecklistForm onSubmit={props.onAddSubchecklist} />
        <Button type="submit" onClick={props.onAddBlankSpace}>
          Add Blank Space
        </Button>
        <Button type="submit" onClick={props.onAddSectionTitle}>
          Add Section Title
        </Button>
      </div>
    </div>
  );
};

export default ChecklistEditor;

我发现的一件奇怪的事情是,如果我对子清单状态进行更改,然后对我的代码进行小的更改并保存,nodemon 将重新加载应用程序并且值将更新为它们应该的值。

我真的被困在这上面,不知道发生了什么,所以我会很感激我能得到的任何帮助。谢谢。

编辑: 我应该提到,如果我从传递给 Subchecklist 组件的任何函数中删除 useCallback 挂钩,它会强制组件重新加载和信息已正确更新,但重新渲染会变得过于昂贵。这似乎也是错误的,因为函数更改是导致重新加载的原因,而数据属性本身应该导致它...

我在这里对您的其余代码做出假设,但我觉得这部分很可能是:

const handleDragEnd = useMemoizedCallback(() => {
  props.onReorderSubchecklists(dragItemIndex, draggedOverItemIndex);
}, [props.onReorderSubchecklists, dragItemIndex, draggedOverItemIndex]);

您实际上是直接修改状态,而不是使用setDragItemIndex 和setDraggedOverItemIndex。这会导致正确渲染出现问题。如果我错了,那么我只能猜测你在其他地方犯了同样的错误。

好的,我发现了问题。它与复制嵌套状态对象时扩展运算符的工作方式有关。

简而言之;如果您状态中的嵌套对象用作子组件的道具,则仅对整个对象使用展开运算符不足以复制嵌套属性。这是因为展开运算符创建了一个浅拷贝(一层),嵌套对象仍将引用与以前相同的对象,并且您的 props 不会更新以触发 re-render.

所以如果我的状态对象如下所示,如果我将内容数组映射到多个子组件并希望它们的更改显示在 props 中并触发组件的 re-render 我将拥有复制内容数组进行修改,然后用新值替换旧数组。

const [myObj, setMyObj] = useState({
  name: "MyName",
  id: 1,
  contents: [
    {
      id: 2,
      name: "Nested Name"
    }
  ]
});

错误

let newMyObj = {...myObj};
newMyObj.contents[0].name = "Updated Name";
setMyObj(newMyObj);

let newContents = [...myObj.contents];
newContents[0].name = "Updated Name";
let newMyObj = {...myObj};
newMyObj.contents = newContents;
setMyObj(newMyObj);