更新 State 中的对象:不可变性与展开语法复制
在 React 中,State 可以存放任何类型的 JavaScript 值,包括数字、字符串、布尔值,也包括对象(Objects)。
但是,当你把一个对象放进 State 之后,你必须遵守一条极为严苛的原则: 永远将 State 中的对象视为只读的,切勿直接修改(变异 Mutation)原对象!
1. 什么是变异(Mutation)?为什么它在 React 里是禁忌?
在 JavaScript 中,技术上你可以随时这样写:
const [position, setPosition] = useState({ x: 0, y: 0 });
function handlePointerMove(e) {
position.x = e.clientX; // 直接修改了内存中原对象的属性!
position.y = e.clientY;
}
这种直接对已经存在的对象属性重新赋值的操作,被称为 变异(Mutation)。
为什么这样做在 React 里完全失效?
- React 依靠对象引用的改变来判断是否发生了更新:
在底层,React 使用
Object.is比较新旧状态。如果直接变异了原对象,新对象和旧对象指向的依然是内存中的同一块地址(引用完全相同),React 就会以为“什么都没有变”,从而根本不会触发重新渲染! - 破坏了快照与调试: 直接变异对象会污染过去渲染的所有历史快照,导致组件的时间旅行、性能优化(React.memo)和调试工具全部失灵。
正道做法:永远不要修改旧对象,而是创建一个全新的对象,并将新对象传给
set函数!
function handlePointerMove(e) {
setPosition({
x: e.clientX,
y: e.clientY
});
}
2. 使用对象展开语法(…)复制并更新局部属性
通常情况下,一个对象里包含很多个属性(比如用户的姓名、邮箱、头像、年龄)。当用户在表单里修改了姓名时,你显然希望保留其他的属性不变。
如果每个属性都手动重新手敲一遍会非常繁琐:
setPerson({
firstName: e.target.value,
lastName: person.lastName,
email: person.email
});
为了简化这个过程,我们使用现代 JavaScript 的 对象展开语法(Spread Syntax ...):
setPerson({
...person, // 复制现有 person 对象的所有属性
firstName: e.target.value // 单独覆盖更新 firstName
});
语法说明: 属性的顺序非常关键!
...person必须放在最前面。写在后面的属性会覆盖前面复制过来的同名属性。
3. 处理嵌套对象的更新
如果对象内部还嵌套了另一个对象,比如:
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Paris',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
如果你想修改作品的城市 city:
注意:展开语法只是“浅拷贝(Shallow Copy)”!
如果只写 { ...person, artwork: { city: 'Tokyo' } },原本作品里的 title 和 image 就会全被丢弃!
要安全更新嵌套对象,必须层层展开拷贝:
setPerson({
...person, // 拷贝外层对象属性(保留 name)
artwork: {
...person.artwork, // 拷贝内层 artwork 的所有属性(保留 title 和 image)
city: 'Tokyo' // 仅覆盖更新 city
}
});
总结
- 将 React State 中的所有对象视为不可变(Immutable)的只读数据;
- 永远不要直接
obj.x = 1,这不会触发重新渲染; - 使用对象展开语法
{ ...obj, x: 1 }生成一个属性被安全覆盖的新对象; - 更新嵌套对象时,需要由外到内逐层展开拷贝。