useReducer实战指南:简化复杂状态管理的正确姿势
项目初期的技术选型
上个月搞一个表单配置器,用户可以在页面上拖拽字段、设置校验规则、调整顺序,最后生成一份结构化配置。一开始我用 useState 管理整个 state,想着不就是个对象嘛,直接 setState 就行了。结果写着写着发现不对劲:表单字段一多,每次改一个字段的某个属性(比如 label),就得深拷贝整个 state,性能肉眼可见地卡顿。
更头疼的是逻辑分散。比如“删除字段”这个操作,既要删字段本身,又要更新所有后续字段的索引,还得处理关联的校验规则。这些逻辑散落在不同地方,改一处漏一处,测试时总出问题。后来一拍脑袋:这不就是 useReducer 的典型场景吗?状态变更复杂、有明确 action 语义、需要集中管理——赶紧切。
核心代码就这几行
先贴 reducer 函数,这是整个逻辑的核心:
const formReducer = (state, action) => {
switch (action.type) {
case 'ADD_FIELD': {
const newField = {
id: Date.now().toString(),
type: action.payload.type,
label: '',
required: false,
// 其他默认属性...
};
return {
...state,
fields: [...state.fields, newField],
};
}
case 'UPDATE_FIELD': {
const { id, updates } = action.payload;
return {
...state,
fields: state.fields.map(field =>
field.id === id ? { ...field, ...updates } : field
),
};
}
case 'DELETE_FIELD': {
const { id } = action.payload;
const indexToDelete = state.fields.findIndex(f => f.id === id);
if (indexToDelete === -1) return state;
const newFields = state.fields.filter(f => f.id !== id);
// 更新后续字段的 displayOrder(如果用了顺序索引)
const updatedFields = newFields.map((field, idx) => ({
...field,
displayOrder: idx,
}));
return {
...state,
fields: updatedFields,
};
}
case 'MOVE_FIELD': {
const { fromIndex, toIndex } = action.payload;
const newFields = [...state.fields];
const [moved] = newFields.splice(fromIndex, 1);
newFields.splice(toIndex, 0, moved);
// 同样需要重置 displayOrder
return {
...state,
fields: newFields.map((f, i) => ({ ...f, displayOrder: i })),
};
}
default:
return state;
}
};
然后在组件里用:
const FormEditor = () => {
const [state, dispatch] = useReducer(formReducer, {
fields: [],
// 其他顶层状态...
});
const handleAddField = (type) => {
dispatch({ type: 'ADD_FIELD', payload: { type } });
};
const handleUpdateField = (id, updates) => {
dispatch({ type: 'UPDATE_FIELD', payload: { id, updates } });
};
// 其他 handler...
};
看起来挺清爽对吧?但实际开发时坑可不少。
最大的坑:性能问题
切完 useReducer 本以为万事大吉,结果一测发现:每次输入框打字(触发 UPDATE_FIELD)还是会卡!明明只改了一个字段的 label,但整个 fields 数组都重新渲染了。后来用 React DevTools 一看,好家伙,所有字段组件都在 re-render。
原因很简单:reducer 里每次返回的都是新数组([...state.fields, newField] 或 map 生成的新数组)。即使内容没变,引用也变了,React 认为所有子组件都需要更新。
折腾了半天,试了几个方案:
- 方案1:用 immer。确实能简化不可变操作,但性能问题没解决,因为 immer 底层还是生成新对象。
- 方案2:缓存字段组件。给每个字段组件加
React.memo,并确保传入的 props 引用稳定。但handleUpdateField这种函数每次 render 都会变,得配合useCallback:
const handleUpdateField = useCallback((id, updates) => {
dispatch({ type: 'UPDATE_FIELD', payload: { id, updates } });
}, [dispatch]);
这招有效,但治标不治本。如果 fields 数组本身引用不变,根本不需要 memo。于是想到终极方案:只在必要时才生成新数组。
改造 UPDATE_FIELD:
case 'UPDATE_FIELD': {
const { id, updates } = action.payload;
const fieldIndex = state.fields.findIndex(f => f.id === id);
if (fieldIndex === -1) return state;
const oldField = state.fields[fieldIndex];
const newField = { ...oldField, ...updates };
// 浅比较:如果字段内容没变,直接返回原 state
let hasChanged = false;
for (const key in updates) {
if (newField[key] !== oldField[key]) {
hasChanged = true;
break;
}
}
if (!hasChanged) return state;
const newFields = [...state.fields];
newFields[fieldIndex] = newField;
return { ...state, fields: newFields };
}
这样只有当字段真发生变化时,fields 数组才会变。配合 React.memo,输入框打字再也不卡了。不过这个浅比较逻辑有点啰嗦,后来封装了个工具函数。
另一个麻烦:异步操作怎么塞进去
项目里有个需求:保存配置时要调 API,成功后再更新本地 state 的 savedAt 字段。一开始我想在 dispatch 后直接 await,但 dispatch 是同步的,没法等 API 结果。
常见的做法是在组件里处理异步:
const handleSave = async () => {
try {
const config = { fields: state.fields };
await fetch('https://jztheme.com/api/save', {
method: 'POST',
body: JSON.stringify(config),
});
dispatch({ type: 'SET_SAVED_AT', payload: Date.now() });
} catch (err) {
// 处理错误
}
};
这没问题,但有些复杂逻辑(比如保存失败后回滚)就得在组件里写一堆状态判断。后来参考 Redux 的 middleware 思路,搞了个简单的 thunk 支持:
// 自定义 hook
const useThunkReducer = (reducer, initialState) => {
const [state, dispatch] = useReducer(reducer, initialState);
const enhancedDispatch = useCallback((action) => {
if (typeof action === 'function') {
return action(dispatch, state);
}
return dispatch(action);
}, [state, dispatch]);
return [state, enhancedDispatch];
};
// 使用
const [state, dispatch] = useThunkReducer(formReducer, initialState);
// 在组件里
const handleSave = () => {
dispatch(async (dispatch, state) => {
const config = { fields: state.fields };
const res = await fetch('https://jztheme.com/api/save', {
method: 'POST',
body: JSON.stringify(config),
});
if (res.ok) {
dispatch({ type: 'SET_SAVED_AT', payload: Date.now() });
}
});
};
这招让异步逻辑也能走 reducer 的流程,代码更集中。不过后来想想,其实对于简单项目可能有点过度设计,直接在组件里处理异步也够用。
回顾与反思
最终效果还不错:状态逻辑集中了,性能问题解决了,连新人接手都说“这代码看得懂”。但有几个地方现在看还能优化:
- action 类型字符串容易拼错。应该用常量或者 TypeScript 的 enum,但项目是 JS 写的,偷懒没做。
- reducer 太大。现在所有逻辑挤在一个文件,超过 200 行了。其实可以按功能拆成多个 reducer 再合并,但当时赶工期就没动。
- 那个浅比较逻辑还是有点糙。如果有嵌套对象更新(比如字段的 options 属性是个数组),现在的比较会失效。不过目前业务没遇到,先不管了。
总的来说,useReducer 在这个项目里救了我一命。它不适合所有场景——简单状态用 useState 更快——但一旦状态变更逻辑复杂起来,useReducer 的收益就非常明显。核心就一点:把“怎么变”从组件里抽出来,变成纯函数,问题就清晰多了。
以上是我踩坑后的总结,希望对你有帮助。如果你有更好的优化方案,比如怎么优雅处理嵌套更新,欢迎评论区交流!

暂无评论