React state 的主要规则是不要直接修改 state。这包括保存在顶级状态对象中的对象,或保存在其中的对象等。因此,要修改您的嵌套对象并使 React 可靠地处理结果,您必须复制您更改的每一层。(是的,真的。下面的详细信息,带有文档链接。)
另外,当您根据现有状态更新状态时,最好使用状态设置器的回调版本,因为状态更新可能是异步的(我不知道他们为什么说“可能在那里”,它们是异步)和状态更新被合并,所以使用旧的状态对象会导致陈旧的信息被放回状态。
考虑到这一点,让我们看看您的第二个更改处理程序(因为它比第一个更深入),它需要 update stateObject.top_level_prop[0].nestprop4[0].deepNestProp1
。为了正确地做到这一点,我们必须复制我们正在修改的最深对象 ( stateObject.top_level_prop[0].nestprop4[0]
) 及其所有父对象;其他对象可以重复使用。所以那是:
stateObject
top_level_prop
top_level_prop[0]
top_level_prop[0].nestprop4
top_level_prop[0].nestprop4[0]
那是因为它们都通过更改top_level_prop[0].nestprop4[0].deepNestProp1
.
所以:
onChange={({target: {value}}) => {
// Update `stateObject.top_level_prop[0].nestprop4[0].deepNestProp1`:
setStateObject(prev => {
// Copy of `stateObject` and `stateObject.top_level_prop`
const update = {
...prev,
top_level_prop: prev.top_level_prop.slice(), // Or `[...prev.top_level_prop]`
};
// Copy of `stateObject.top_level_prop[0]` and `stateObject.top_level_prop[0].nextprop4`
update.top_level_prop[0] = {
...update.top_level_prop[0],
nextprop4: update.top_level_prop[0].nextprop4.slice()
};
// Copy of `stateObject.top_level_prop[0].nextprop4[0]`, setting the new value on the copy
update.top_level_prop[0].nextprop4[0] = {
...update.top_level_prop[0].nextprop4[0],
deepNestProp1: value
};
return update;
});
}}
最好不要复制树中未更改的其他对象,因为渲染它们的任何组件都不需要重新渲染,但需要复制我们正在更改的最深对象及其所有父对象。
围绕这一点的尴尬是useState
在可能的情况下保持使用小状态对象的原因之一。
但我们真的必须这样做吗?
是的,让我们看一个例子。下面是一些没有做必要副本的代码:
const {useState} = React;
const ShowNamed = React.memo(
({obj}) => <div>name: {obj.name}</div>
);
const Example = () => {
const [outer, setOuter] = useState({
name: "outer",
middle: {
name: "middle",
inner: {
name: "inner",
},
},
});
const change = () => {
setOuter(prev => {
console.log("Changed");
prev.middle.inner.name = prev.middle.inner.name.toLocaleUpperCase();
return {...prev};
});
};
return <div>
<ShowNamed obj={outer} />
<ShowNamed obj={outer.middle} />
<ShowNamed obj={outer.middle.inner} />
<input type="button" value="Change" onClick={change} />
</div>;
};
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
请注意单击按钮似乎没有做任何事情(除了记录“已更改”),即使状态已更改。那是因为传递给的对象ShowName
没有改变,所以ShowName
没有重新渲染。
这是进行必要更新的一个:
const {useState} = React;
const ShowNamed = React.memo(
({obj}) => <div>name: {obj.name}</div>
);
const Example = () => {
const [outer, setOuter] = useState({
name: "outer",
middle: {
name: "middle",
inner: {
name: "inner",
},
},
});
const change = () => {
setOuter(prev => {
console.log("Changed");
const update = {
...prev,
middle: {
...prev.middle,
inner: {
...prev.middle.inner,
name: prev.middle.inner.name.toLocaleUpperCase()
},
},
};
return update;
});
};
return <div>
<ShowNamed obj={outer} />
<ShowNamed obj={outer.middle} />
<ShowNamed obj={outer.middle.inner} />
<input type="button" value="Change" onClick={change} />
</div>;
};
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
该示例用于React.memo
避免在其 props 未更改时重新渲染子组件。同样的事情发生在PureComponent
或 任何实现shouldComponentUpdate
并且在其props没有改变时不更新的组件。
React.memo
/ PureComponent
/shouldComponentUpdate
在主要代码库(和抛光组件)使用,以避免不必要的重新呈现。朴素的不完整状态更新会在使用它们时咬你,在其他时候也可能如此。