在 React 中,状态(state)被视为不可变数据。这意味着你不应该直接修改 state 的值,而是要通过 setState 或 useState 的更新函数来返回一个新的值。直接修改 state 是 React 开发中最常见的反模式之一,看似“无害”,实则暗藏多个隐患。
为什么直接修改是危险的?
1. 视图不会更新
React 通过浅比较(Shallow Compare)判断状态是否变化。如果你直接修改了 state 对象的属性,由于引用地址未变,React 会认为状态没有更新,从而跳过重新渲染。
// ❌ 错误:直接修改 state
const [user, setUser] = useState({ name: 'Alice', age: 25 });
user.name = 'Bob';
setUser(user); // React 对比发现 user 引用未变,不会重新渲染
正确做法是创建一个新对象:
// ✅ 正确:通过展开运算符创建新对象
setUser({ ...user, name: 'Bob' });
2. 导致不可预测的 Bug
直接修改 state 可能会在异步场景下产生竞态问题。由于 React 的批量更新机制,多次直接修改可能互相覆盖,最终丢失部分更新。
for (let i = 0; i < 3; i++) {
user.age += 1; // 直接修改同一个对象
setUser(user); // 每次 setUser 引用相同,最终只生效一次
}
// 期望 age 为 28,实际只增加了 1
3. 破坏 React 的不可变数据模型
不可变数据是 React 性能优化(如 React.memo、PureComponent)和 Hooks(如 useEffect 依赖项)的基石。一旦破坏这个约定,依赖浅比较的优化全部失效,甚至导致副作用异常。
useEffect(() => {
console.log('user changed');
}, [user]); // 如果直接修改 user,依赖项对比始终为 true,副作用不再执行
4. 并发模式下的状态撕裂
React 18 引入并发渲染后,状态更新可能被打断并重试。如果直接修改共享状态,可能读到中间态的不一致数据,引发难以调试的撕裂问题。
常见陷阱与正确做法
数组操作
// ❌ 直接 push,返回原数组引用
const [items, setItems] = useState([]);
items.push(newItem);
setItems(items);
// ✅ 返回新数组
setItems([...items, newItem]);
// 或使用函数式更新(推荐依赖旧状态时使用)
setItems(prev => [...prev, newItem]);
嵌套对象
// ❌ 深层直接修改
const [profile, setProfile] = useState({ info: { name: 'Alice' } });
profile.info.name = 'Bob';
setProfile(profile);
// ✅ 使用展开运算符浅拷贝(对于深层嵌套推荐使用 immer 等库)
setProfile({
...profile,
info: { ...profile.info, name: 'Bob' }
});
删除数组项
// ❌ 直接 splice,修改原数组
const newItems = items;
newItems.splice(index, 1);
setItems(newItems);
// ✅ 使用 filter 返回新数组
setItems(items.filter((_, i) => i !== index));
如何彻底避免?
- 始终使用扩展运算符或 map/filter/concat 等创建新引用。
- 使用函数式更新:当新状态依赖旧状态时,使用
setState(prev => newValue)。 - 复杂状态使用 useReducer:Reducer 的不可变逻辑更清晰。
- 引入 Immer 简化不可变操作:通过
produce函数以“可变”语法生成不可变数据。 - 开启 ESLint 规则:如
react/no-direct-mutation-state能在开发阶段捕获这类错误。
直接修改 state 是一个很容易犯的错,尤其是在数据嵌套较深时。但理解其背后的不可变原理,并养成对应的编码习惯,就能从根本上避免这个隐患,写出更可靠、更易维护的 React 代码。