React中手动修改状态为何是反模式?附复杂状态场景
假设我们有一个包含大量复制成本高昂字段的复杂状态,需要按特定顺序更新,常规写法如下:
setState({ ...myComplexState, expensiveFieldA: newA, }); // 计算newB的逻辑 setState({ ...myComplexState, expensiveFieldB: newB, });
这种写法会触发多次重渲染,还会在复制未更改字段上浪费CPU资源,因此有人想出了下面这种直接修改状态再手动触发更新的模式:
import { useState } from 'react'; class StateWrapper<S> { state: S; constructor(state: S) { this.state = state; } shallowCopy() { return new StateWrapper(this.state); } } function useObjState<T>(initState: T): [T, () => void] { const [wrapper, setWrapper] = useState(() => new StateWrapper(initState)); const commit = () => setWrapper(wrapper.shallowCopy()); return [wrapper.state, commit]; } class ExpensiveState { private val: string; constructor() { this.val = ''; } preformExpensiveOperations(val: string) { this.val = val; } preformAnotherExpensiveOperation() {} getVal() { return this.val; } } function App() { const [state, commit] = useObjState(new ExpensiveState()); return ( <> <p>val: {state.getVal()}</p> <p> <input onChange={e => { state.preformExpensiveOperations(e.target.value); state.preformAnotherExpensiveOperation(); commit(); }} /> </p> </> ); } export default App;
虽然这种写法看似解决了性能问题,但它是明确的反模式,原因如下:
1. 违反React的不可变状态原则
React的更新机制完全依赖状态引用的变化来识别状态更新。直接修改状态对象的内部值,状态的引用并没有改变,这会导致依赖状态的优化逻辑(比如React.memo、shouldComponentUpdate)失效——这些逻辑会认为状态没有变化,从而跳过必要的重渲染,最终造成UI和实际状态不一致。
2. 并发渲染下的状态一致性风险
React 18+引入了并发渲染模式,渲染过程可能被中断、暂停或重启。直接修改同一个状态引用时,不同渲染周期可能拿到被篡改的中间状态,导致UI出现不可预测的错误,比如显示过时的数据、操作结果异常等。
3. 调试难度指数级上升
React DevTools依赖状态的不可变性来记录每一次状态变更的历史。直接修改状态的话,DevTools无法追踪到状态的具体修改过程,你看不到状态从A到B的变化轨迹,排查bug时只能盲目猜测,效率极低。
4. 破坏组件的复用与可维护性
这种写法把状态的修改逻辑耦合在自定义类中,和React函数式组件的设计理念相悖。其他组件如果要复用这个状态,很难进行扩展,而且状态的修改过程完全不透明,违背了React单向数据流的原则,长期来看会让代码的维护成本急剧上升。
5. 隐藏的性能隐患
看似减少了对象复制的成本,但实际上这种写法会让React的更新逻辑混乱。比如多次修改状态后调用commit,可能因为状态引用的问题导致意外的重渲染;或者因为状态未被正确标记为更新,需要额外的渲染来修正UI,反而比常规写法的性能更差。
内容的提问来源于stack exchange,提问作者Winston Moxley

