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);
我正在从事的一个项目涉及一个大型组件树,向下传递来自顶部组件中大型复杂状态对象的道具。
在一个特定的组件“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);