前端状态管理常见 Bug:多源状态冲突的诊断与修复
问题模式:多源状态更新冲突
这是 React/Solid/Vue 等框架中最常见的一类 bug——单一状态被多个来源修改,但每个来源都不知道对方的存在。典型表现:功能"时而好用时而不好用",因为冲突触发条件取决于执行顺序。
实战案例:侧边栏折叠状态冲突
原始代码(有 Bug)
const [collapsed, setCollapsed] = useState(false);
// 来源一:响应式断点
useEffect(() => {
const handleResize = () => setCollapsed(window.innerWidth < 1024);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// 来源二:用户手动点击
const toggle = () => setCollapsed(prev => !prev);
// Bug 复现步骤:
// 1. 大屏幕下,用户点按钮展开侧边栏 → collapsed = false
// 2. 缩小窗口到 < 1024px → resize 事件触发 → collapsed = true(自动折叠)
// 3. 用户点按钮想展开 → toggle 执行 → collapsed = false ✓
// 4. 放大窗口回来 → resize 事件触发 → collapsed = false(屏幕宽了自动展开)
// 5. 用户点按钮想折叠 → 但 collapsed 已经是 false → 变成 true(展开)
// 6. 用户再点 → 变成 false → 看起来"没反应"!
根因分析
两个状态修改源(响应式断点 + 用户点击)在修改同一个 collapsed 状态,但都没有考虑对方的存在。当 resize 事件在用户操作之后触发时,会覆盖用户的意图。
修复方案:引入手动覆盖标志
const [collapsed, setCollapsed] = useState(false);
const [manualOverride, setManualOverride] = useState(false);
// 仅在非手动覆盖时响应屏幕尺寸
useEffect(() => {
const handleResize = () => {
if (!manualOverride) {
setCollapsed(window.innerWidth < 1024);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [manualOverride]);
// 用户操作时设置覆盖标志
const toggle = () => {
setCollapsed(prev => !prev);
setManualOverride(true);
};
// 可选:提供"重置"按钮取消手动覆盖
const resetAuto = () => {
setManualOverride(false);
setCollapsed(window.innerWidth < 1024);
};
通用解决方案:useReducer
对于复杂的状态交互,useReducer 比多个 useState 更清晰,因为所有的状态变更都集中在 reducer 中,逻辑内聚:
const initialState = { collapsed: false, mode: 'auto' };
function sidebarReducer(state, action) {
switch (action.type) {
case 'RESIZE':
// auto 模式:响应断点;manual 模式:忽略
return state.mode === 'auto'
? { ...state, collapsed: action.isSmall }
: state;
case 'TOGGLE':
// 用户操作:切换到 manual 模式
return { collapsed: !state.collapsed, mode: 'manual' };
case 'RESET':
return { collapsed: action.isSmall, mode: 'auto' };
default:
return state;
}
}
预防原则
- 单一状态源原则:如果多个来源修改同一状态,必须引入"优先级/覆盖"机制
- 用 useReducer 替代多 useState:复杂交互下 reducer 的逻辑内聚性远超分散的 useState
- 始终用本地 dev server + 浏览器实际交互测试:代码逻辑正确不代表交互正常
- 考虑边缘情况:resize、网络变化、快速点击、并发操作都是 bug 高发区