哎,兄弟,咱们来聊点实在的。
最近我带几个实习生,发现他们从 Redux 转 React 18 的 useReducer 时,踩坑的姿势千奇百怪。有的把 Redux 的样板代码原封不动搬过来,结果发现状态根本更新不上;有的以为 useReducer 和 setState 一样透明,结果写出了一堆副作用耦合的烂代码。
今天咱们不整那些虚头巴脑的定义,直接切入痛点,用真实案例告诉你:怎么少踩坑,怎么写出让人眼前一亮的 Reducer。
一、先别急着写代码,先纠正两个致命误解
很多新手一上来就想写 dispatch,结果代码写得像屎山。在动笔之前,你得先搞清楚 useReducer 到底是谁,它和 Redux 有啥本质区别。
误解 1:“useReducer 就是没有中间件的 Redux”
错大发了。Redux 是一个独立的状态容器,有 Store、有中间件、有 DevTools。而 useReducer 只是 React 的一个 Hook,它帮你把组件内的状态逻辑抽离出来。
- Redux:状态在组件树之外,全局共享,适合大型应用。
- useReducer:状态在组件内部(或者通过 Context 共享),适合中大型组件的状态管理,或者当你
setState变得难以维护时。
如果你指望用 useReducer 搞定全局状态,那你还是老老实实去用 Zustand 或者 Redux Toolkit 吧。useReducer 的定位是“复杂组件内部的状态管理器”。
误解 2:“Reducer 里可以写任何逻辑,包括网络请求”
这是最最常见的坑!Redux 的 Action Creators 可以异步,useReducer 的 dispatch 也可以触发异步操作,但是——Reducer 函数本身必须是纯函数!
纯函数:同样的输入,永远返回同样的输出;没有任何副作用(不修改外部状态,不发请求,不打印日志)。
如果你把 fetch 或者 setTimeout 塞进 Reducer 里,你的应用会疯掉的。
// ❌ 错误示范:Reducer 里干了坏事
function reducer(state, action) {
switch (action.type) {
case 'FETCH_DATA':
// 千万别这么干!Reducer 里不能发请求
fetch('/api/data')
.then(res => res.json())
.then(data => {
// 这行代码会触发多次渲染,而且状态更新不可预测
dispatch({ type: 'DATA_LOADED', payload: data });
});
return state; // 返回个寂寞?
default:
return state;
}
}
那怎么弄?把副作用放在 dispatch 之后,用 useEffect 或者事件处理函数来处理。
// ✅ 正确示范:副作用与状态逻辑分离
function MyComponent() {
const [state, dispatch] = useReducer(reducer, initialState);
// 事件处理函数里发起请求
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => {
dispatch({ type: 'DATA_LOADED', payload: data });
});
}, []); // 只在挂载时执行一次
return <div>{state.data}</div>;
}
// Reducer 只负责根据 Action 计算新状态
function reducer(state, action) {
switch (action.type) {
case 'DATA_LOADED':
return { ...state, data: action.payload };
default:
return state;
}
}
二、新手最容易踩的四个坑(附避坑指南)
坑 1:直接修改 State 对象(Mutating State)
这是 Redux 时代的遗留问题,但在 useReducer 里更容易犯,因为你容易忘记 spread operator(...)。
React 依靠引用比较来判断状态是否变化。如果你直接修改了 state 对象的属性,React 认为状态没变,就不会重新渲染。
// ❌ 错误:直接修改
function reducer(state, action) {
if (action.type === 'ADD_TODO') {
state.todos.push(action.todo); // Mutation!
return state; // React: "没变化啊,干嘛要我重渲?"
}
return state;
}
// ✅ 正确:返回新对象
function reducer(state, action) {
if (action.type === 'ADD_TODO') {
return {
...state,
todos: [...state.todos, action.todo] // 创建新数组
};
}
return state;
}
避坑技巧:
- 开启 ESLint 的
react-hooks/exhaustive-deps和 Redux Toolkit 推荐的no-mutating-state规则。 - 如果状态嵌套很深,手动 spread 会累死,这时候可以考虑用 Immer 库。Immer 让你“看起来”在修改状态,但实际上它会在底层创建不可变的新状态。
// 使用 Immer 的用法(非常推荐)
import { produce } from 'immer';
function reducer(state, action) {
return produce(state, draft => {
switch (action.type) {
case 'ADD_TODO':
draft.todos.push(action.todo); // 看起来像修改,实际上是不可变的
break;
case 'UPDATE_TODO':
const todo = draft.todos.find(t => t.id === action.id);
if (todo) todo.completed = !todo.completed;
break;
default:
break;
}
});
}
坑 2:Action Type 写错,或者 Action 结构混乱
Redux 里大家习惯用字符串常量(CREATE_TODO),但在 useReducer 里,你可以更灵活。不过,很多新手会把 Action 定义得过于复杂,或者类型命名不规范。
避坑技巧:
- Action 类型常量化的好处:避免拼写错误。虽然
useReducer不强制,但养成好习惯总是没错的。 - Payload 结构清晰:Action 的结构应该是
{ type, payload }。如果参数很多,用对象包裹。
// ✅ 推荐的 Action 结构
const ACTIONS = {
INCREMENT: 'INCREMENT',
DECREMENT: 'DECREMENT',
SET_USER: 'SET_USER',
FETCH_START: 'FETCH_START',
FETCH_SUCCESS: 'FETCH_SUCCESS',
FETCH_FAILURE: 'FETCH_FAILURE',
};
function reducer(state, action) {
switch (action.type) {
case ACTIONS.INCREMENT:
return { ...state, count: state.count + 1 };
case ACTIONS.SET_USER:
// payload 是一个用户对象
return { ...state, user: action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
注意:一定要加 default 分支并抛出错误,这样当 Action 类型写错时,你能立刻在控制台看到报错,而不是静默失败(返回旧状态)。
坑 3:在 Reducer 中依赖外部变量
Reducer 应该是纯函数,它不应该知道 React 组件里有什么变量,也不应该知道全局状态(除非通过 State 传入)。
// ❌ 错误:依赖外部变量
let externalCounter = 0;
function reducer(state, action) {
if (action.type === 'ADD') {
return { ...state, count: state.count + externalCounter }; // 糟糕!
}
return state;
}
避坑技巧:
如果确实需要“外部”信息,要么把它放入 State,要么通过 dispatch 的 payload 传入。
// ✅ 正确:通过 Payload 传入
dispatch({ type: 'ADD', delta: 5 });
function reducer(state, action) {
if (action.type === 'ADD') {
return { ...state, count: state.count + action.payload.delta };
}
return state;
}
坑 4:把 useReducer 和 useState 混着用,导致状态不一致
一个组件里,有些状态用 useState,有些用 useReducer,有时候还会互相依赖。这会导致逻辑分散,难以调试。
避坑技巧:
- 原则:如果一组状态经常一起变化,或者状态更新逻辑复杂(需要基于前一个状态计算),就用
useReducer。 - 简单状态:如果只是一个开关、一个输入框的值,用
useState就够了,别为了用而用。 - 统一状态树:即使使用了
useReducer,也建议尽量将相关联的状态合并到一个 State 对象中,而不是拆分成多个 Reducer。
// ❌ 糟糕:状态碎片化
const [count, setCount] = useState(0);
const [userName, setUserName] = useState('');
const [isLoading, setIsLoading] = useState(false);
// ✅ 更好:逻辑相关的状态合在一起
const [authState, dispatch] = useReducer(authReducer, {
user: null,
isLoading: false,
error: null,
});
const [count, setCount] = useState(0); // 独立的计数器,用 useState 没问题
三、如何正确编写一个健壮的 Reducer 函数?
好了,坑都排除了,咱们来看看怎么写一个“别人看了想给你点赞”的 Reducer。
1. 初始状态要定义清楚
初始状态(Initial State)应该是所有可能状态的一个“快照”。如果状态来自服务端,记得给一个合理的默认值(比如空数组、null、0),避免组件渲染时出现“闪屏”或报错。
const initialState = {
users: [],
isLoading: false,
error: null,
// 嵌套状态也要初始化,避免 undefined 错误
filters: {
search: '',
status: 'all',
}
};
2. 使用 const 定义 Reducer 函数
不要用 function 关键字声明,用 const。这样更符合 Hook 的使用习惯,也便于后续的重构和测试。
// ✅ 推荐
const userReducer = (state, action) => {
// ...
};
3. Action 命名规范:动词 + 名词
Redux 风格推荐 USER_FETCH_REQUESTED,但 useReducer 可以更简洁。建议用 过去分词 或 现在分词 描述状态变化,或者直接描述动作。
ADD_TODOTOGGLE_TODOFETCH_USERS_STARTFETCH_USERS_SUCCESS
4. 利用 TypeScript 增强类型安全(强烈建议)
如果你是用 TypeScript,一定要给 useReducer 加上类型。这能帮你抓住 90% 的运行时错误。
// 定义 State 类型
interface UserState {
users: User[];
isLoading: boolean;
error: string | null;
}
// 定义 Action 类型
type UserAction =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: User[] }
| { type: 'FETCH_FAILURE'; payload: string };
// 定义 Reducer
const userReducer = (state: UserState, action: UserAction): UserState => {
switch (action.type) {
case 'FETCH_START':
return { ...state, isLoading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, isLoading: false, users: action.payload };
case 'FETCH_FAILURE':
return { ...state, isLoading: false, error: action.payload };
default:
// TypeScript 会检查这里是否覆盖了所有情况
const exhaustiveCheck: never = action;
throw new Error(`Unhandled action type: ${exhaustiveCheck}`);
}
};
5. 处理复杂嵌套状态:Immer 是神器
回到 Imre,我再强调一次。当你的 State 有三层以上嵌套时,手动 spread 会让人崩溃,而且极易出错。
// 不用 Immer,你要写这样的代码
return {
...state,
filters: {
...state.filters,
advanced: {
...state.filters.advanced,
sortBy: action.payload // 假设你只想改这一个字段
}
}
};
// 用 Immer,代码清爽得多
import { produce } from 'immer';
const reducer = produce((draft, action) => {
switch (action.type) {
case 'UPDATE_FILTER':
draft.filters.advanced.sortBy = action.payload;
break;
// ...
}
});
6. 拆分 Reducer:当逻辑过于复杂时
如果一个 Reducer 超过 100 行,或者处理了太多不相干的状态,考虑拆分。
方法一:Context + 多个 useReducer
将相关状态分组,每个组用一个 useReducer 管理,然后通过 Context 传递。
方法二:Redux Toolkit 的 createSlice
如果你最终还是觉得 useReducer 不够用,或者状态需要在多个组件间共享,那么 Redux Toolkit 的 createSlice 是目前最推荐的方案。它内部使用了 Immer,自动处理 immutable update,并且有 Redux DevTools 支持。
// Redux Toolkit 示例
import { createSlice } from '@reduxjs/toolkit';
const userSlice = createSlice({
name: 'users',
initialState,
reducers: {
addUser: (state, action) => {
// 这里可以用 Immer 写法!
state.users.push(action.payload);
},
toggleUser: (state, action) => {
const user = state.users.find(u => u.id === action.payload);
if (user) user.active = !user.active;
},
},
});
四、实战案例:一个完整的 Todo 应用
咱们来个实际的,把上面的知识点串起来。假设我们要做一个 Todo 列表,支持添加、删除、切换完成状态、筛选。
1. 定义类型(TypeScript)
type TodoStatus = 'all' | 'active' | 'completed';
interface Todo {
id: string;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
filter: TodoStatus;
loading: boolean;
error: string | null;
}
2. 定义 Action
type TodoAction =
| { type: 'ADD_TODO'; payload: string }
| { type: 'TOGGLE_TODO'; payload: string }
| { type: 'DELETE_TODO'; payload: string }
| { type: 'SET_FILTER'; payload: TodoStatus }
| { type: 'LOAD_START' }
| { type: 'LOAD_SUCCESS'; payload: Todo[] }
| { type: 'LOAD_FAILURE'; payload: string };
3. 编写 Reducer
import { produce } from 'immer';
const initialState: TodoState = {
todos: [],
filter: 'all',
loading: false,
error: null,
};
const todoReducer = produce((draft: TodoState, action: TodoAction) => {
switch (action.type) {
case 'ADD_TODO':
draft.todos.push({
id: Date.now().toString(),
text: action.payload,
completed: false,
});
break;
case 'TOGGLE_TODO':
const todoToToggle = draft.todos.find(t => t.id === action.payload);
if (todoToToggle) {
todoToToggle.completed = !todoToToggle.completed;
}
break;
case 'DELETE_TODO':
draft.todos = draft.todos.filter(t => t.id !== action.payload);
break;
case 'SET_FILTER':
draft.filter = action.payload;
break;
case 'LOAD_START':
draft.loading = true;
draft.error = null;
break;
case 'LOAD_SUCCESS':
draft.todos = action.payload;
draft.loading = false;
break;
case 'LOAD_FAILURE':
draft.loading = false;
draft.error = action.payload;
break;
default:
// 检查是否遗漏了 action 类型
const _exhaustiveCheck: never = action;
throw new Error(`Unhandled action type: ${_exhaustiveCheck}`);
}
});
export default todoReducer;
4. 在组件中使用
”`typescript import React, { useReducer, useEffect } from ‘react’; import todoReducer, { initialState } from ‘./todoReducer’;
const TodoApp = () => { const [state, dispatch] = useReducer(todoReducer, initialState); const [inputValue, setInputValue] = React.useState(“);
// 模拟加载数据 useEffect(() => {
dispatch({ type: 'LOAD_START' });
setTimeout(() => {
const mockTodos = [
{ id: '1', text: '学习 React', completed: false },
{ id: '2', text: '学习 Redux', completed: true },
];
dispatch({ type: 'LOAD_SUCCESS', payload: mockTodos });
}, 1000);
}, []);
const handleAdd = () => {
if (!inputValue.trim()) return;
dispatch({ type: 'ADD_TODO', payload: inputValue });
setInputValue('');
};
// 渲染逻辑… return (
<div>
<h1>Todo 列表</h1>
{state.loading && <p>加载中...</p>}
{state.error && <p style={{ color: 'red' }}>{state.error}</p