Redux刻意修改状态未出现预期Bug的技术问询
哈哈,太懂这种想刻意搞出bug却“失败”的感觉了!你直接mutate了state但组件还能正常渲染,核心原因是Redux和React-Redux的更新逻辑刚好“放过”了你的操作,咱们一步步拆解:
为啥你的计数器没出bug?
先还原一下你大概率写的reducer代码:
const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; export default function reducer(state = { count: 0 }, action) { switch(action.type) { case INCREMENT_COUNTER: state.count++; // 直接修改原state对象的属性 return state; // 返回同一个state引用 default: return state; } }
你组件里的状态选择器应该是类似这样:
const count = useSelector(state => state.count);
这时候即使你mutate了state,useSelector会比较返回的count值(原始类型,值比较),而不是整个state对象的引用。因为每次count的值确实变了,所以组件会触发重渲染,看起来一切正常——但其实你的state已经被污染了(Redux DevTools里能看到state的历史记录是混乱的,因为mutate会修改过去的state快照)。
如何制造明显的状态不一致Bug?
要让bug显现,你需要利用Redux浅比较引用的特性,让组件“感知不到”state的变化,但实际store里的state已经被修改。试试下面的步骤:
1. 改成嵌套结构的state
把初始state改成嵌套对象,这样mutate内部属性时,外层state的引用不会变:
const initialState = { counter: { value: 0 } };
2. 在reducer里mutate嵌套属性
保持直接修改state的操作:
export default function reducer(state = initialState, action) { switch(action.type) { case INCREMENT_COUNTER: // 直接修改嵌套对象的属性,外层state引用不变 state.counter.value++; return state; default: return state; } }
3. 组件里返回嵌套对象作为选择器结果
把组件的选择器改成返回整个嵌套的counter对象,而不是单独的value:
// 注意:这里返回的是state.counter这个对象的引用 const counter = useSelector(state => state.counter); // 组件里显示: return <div>Count: {counter.value}</div>;
4. 触发bug!
现在点击递增按钮:
- Redux store里的
counter.value已经被修改成了新值(你可以在Redux DevTools里确认) - 但组件不会触发重渲染,因为
useSelector浅比较的是返回的counter对象引用——前后引用完全一样,所以它认为状态没变化 - 如果你再触发其他会修改state的操作(比如加一个“重置”按钮修改另一个state属性),组件会重新渲染,这时候才会显示最新的
counter.value,出现明显的“延迟更新”状态不一致!
另外,你还可以做一个更直观的对比:同时写两个组件,一个用useSelector(state => state.counter),另一个用useSelector(state => state.counter.value)。点击递增按钮后,第二个组件会实时更新,第一个组件却保持旧值,完美展示状态不一致的bug!
额外小贴士
如果你的组件用的是connect高阶组件,要注意mapStateToProps的返回值:如果每次都返回新对象(比如{ count: state.count }),那即使你mutate了state,connect的浅比较也会认为状态变化,组件还是会更新。要制造bug的话,让mapStateToProps返回嵌套的state对象即可。
内容的提问来源于stack exchange,提问作者Topsy

