Hook
React 18+
useReducer: 管理复杂状态
useReducer 是 useState 的替代方案,适用于复杂的状态逻辑
什么是 useReducer?
useReducer 让你使用 reducer 函数管理组件状态。它类似于 Redux 的 reducer 模式,通过 action 来描述状态变化。
TypeScript
import { useReducer } from 'react';
const [state, dispatch] = useReducer(reducer, initialArg, init?);
基本概念
- state: 当前的状态
- dispatch: 触发状态更新的函数
- reducer: 根据旧状态和 action 计算新状态的函数
- action: 描述状态变化的对象(通常有 type 属性)
何时使用 useReducer?
- 状态逻辑复杂,包含多个子值
- 下一个 state 依赖于之前的 state
- 状态更新涉及多个子组件
- 需要更可预测的状态管理
基本用法
Reducer 函数
TypeScript
// reducer 函数签名
function reducer(state, action) {
// 根据 action.type 返回新的 state
switch (action.type) {
case 'ACTION_TYPE':
return { ...state, /* 新的 state */ };
default:
return state;
}
}
简单计数器示例
TypeScript
import { useReducer } from 'react';
// 1. 定义初始状态
const initialState = { count: 0 };
// 2. 定义 reducer
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return initialState;
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
// 3. 使用 useReducer
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>计数: {state.count}</p>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'reset' })}>重置</button>
</div>
);
}
Reducer 必须是纯函数
- 相同输入必定产生相同输出
- 不能有副作用
- 不能修改参数(state),必须返回新对象
useReducer vs useState
对比示例:计数器
TypeScript
// 使用 useState
function Counter({ initialCount = 0 }) {
const [count, setCount] = useState(initialCount);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>加</button>
<button onClick={() => setCount(initialCount)}>重置</button>
</div>
);
}
// 使用 useReducer
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'reset':
return { count: action.payload };
default:
return state;
}
}
function Counter({ initialCount = 0 }) {
const [state, dispatch] = useReducer(reducer, { count: initialCount });
return (
<div>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>加</button>
<button onClick={() => dispatch({ type: 'reset', payload: initialCount })}>
重置
</button>
</div>
);
}
| 特性 | useState | useReducer |
|---|---|---|
| 适用场景 | 简单状态 | 复杂状态逻辑 |
| 状态更新 | 直接设置值 | 通过 dispatch action |
| 更新逻辑 | 分散在组件中 | 集中在 reducer 中 |
| 可测试性 | 较难测试 | 易于测试(纯函数) |
| 代码量 | 较少 | 较多 |
选择建议
- 状态是简单对象/基本类型 →
useState - 状态更新逻辑复杂 →
useReducer - 多个子值需要一起更新 →
useReducer - 下一个 state 依赖于前一个 state →
useReducer
复杂示例:Todo List
TypeScript
import { useReducer, useState } from 'react';
// 1. 定义初始状态
const initialState = {
todos: [],
filter: 'all', // 'all', 'active', 'completed'
};
// 2. 定义 action 类型
const ACTIONS = {
ADD_TODO: 'add_todo',
TOGGLE_TODO: 'toggle_todo',
DELETE_TODO: 'delete_todo',
SET_FILTER: 'set_filter',
EDIT_TODO: 'edit_todo',
};
// 3. 定义 reducer
function todosReducer(state, action) {
switch (action.type) {
case ACTIONS.ADD_TODO: {
const newTodo = {
id: Date.now(),
text: action.payload.text,
completed: false,
};
return {
...state,
todos: [...state.todos, newTodo],
};
}
case ACTIONS.TOGGLE_TODO: {
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
),
};
}
case ACTIONS.DELETE_TODO: {
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload.id),
};
}
case ACTIONS.EDIT_TODO: {
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, text: action.payload.text }
: todo
),
};
}
case ACTIONS.SET_FILTER: {
return {
...state,
filter: action.payload.filter,
};
}
default:
return state;
}
}
// 4. 使用 useReducer
function TodoApp() {
const [state, dispatch] = useReducer(todosReducer, initialState);
const [inputValue, setInputValue] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (inputValue.trim()) {
dispatch({ type: ACTIONS.ADD_TODO, payload: { text: inputValue } });
setInputValue('');
}
};
const getFilteredTodos = () => {
switch (state.filter) {
case 'active':
return state.todos.filter(todo => !todo.completed);
case 'completed':
return state.todos.filter(todo => todo.completed);
default:
return state.todos;
}
};
const filteredTodos = getFilteredTodos();
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="添加新任务..."
/>
<button type="submit">添加</button>
</form>
<div>
<button onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: { filter: 'all' } })}>
全部
</button>
<button onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: { filter: 'active' } })}>
进行中
</button>
<button onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: { filter: 'completed' } })}>
已完成
</button>
</div>
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch({ type: ACTIONS.TOGGLE_TODO, payload: { id: todo.id } })}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => dispatch({ type: ACTIONS.DELETE_TODO, payload: { id: todo.id } })}>
删除
</button>
</li>
))}
</ul>
</div>
);
}
惰性初始化
useReducer 支持惰性初始化,可以选择性地传递第三个参数 init 函数。
TypeScript
// 初始化函数
function init(initialState) {
return {
...initialState,
// 计算初始值
computed: expensiveComputation(),
};
}
// 使用
function Component({ propValue }) {
const [state, dispatch] = useReducer(
reducer,
{ value: propValue },
init // init 函数只在初始渲染时调用
);
// ...
}
示例:从 props 初始化状态
TypeScript
function init(initialCount) {
return { count: initialCount };
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return init(action.payload);
default:
return state;
}
}
function Counter({ initialCount = 0 }) {
const [state, dispatch] = useReducer(
reducer,
initialCount,
init
);
return (
<div>
<p>计数: {state.count}</p>
<button onClick={() => dispatch({ type: 'reset', payload: initialCount })}>
重置
</button>
</div>
);
}
惰性初始化的好处
- 避免每次渲染都重新计算初始值
- 适用于计算量大的初始化逻辑
- init 函数只在初始渲染时调用一次
结合 Context 使用
useReducer 与 Context 结合使用,可以实现类似 Redux 的全局状态管理。
创建 Context
TypeScript
import { createContext, useContext, useReducer } from 'react';
// 1. 创建 Context
const TodosContext = createContext(null);
// 2. 定义 reducer
function todosReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, { id: Date.now(), text: action.payload, completed: false }];
case 'toggle':
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
case 'delete':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
}
// 3. 创建 Provider
function TodosProvider({ children }) {
const [state, dispatch] = useReducer(todosReducer, []);
return (
<TodosContext.Provider value={{ state, dispatch }}>
{children}
</TodosContext.Provider>
);
}
// 4. 创建自定义 Hook
function useTodos() {
const context = useContext(TodosContext);
if (!context) {
throw new Error('useTodos must be used within TodosProvider');
}
return context;
}
// 5. 使用
function TodoList() {
const { state: todos, dispatch } = useTodos();
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => dispatch({ type: 'toggle', id: todo.id })}
/>
{todo.text}
<button onClick={() => dispatch({ type: 'delete', id: todo.id })}>
删除
</button>
</li>
))}
</ul>
);
}
function AddTodo() {
const { dispatch } = useTodos();
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
dispatch({ type: 'add', payload: text });
setText('');
}
};
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button type="submit">添加</button>
</form>
);
}
// 根组件
function App() {
return (
<TodosProvider>
<AddTodo />
<TodoList />
</TodosProvider>
);
}
模式说明
这是实现全局状态管理的常用模式:
- 创建 Context 存储 state 和 dispatch
- 创建 Provider 包裹应用
- 创建自定义 Hook 简化使用
- 在组件中使用 Hook 访问和更新状态
最佳实践
1. 使用常量定义 action 类型
TypeScript
// ✅ 好:使用常量避免拼写错误
const ActionTypes = {
INCREMENT: 'INCREMENT',
DECREMENT: 'DECREMENT',
RESET: 'RESET',
};
dispatch({ type: ActionTypes.INCREMENT });
// ❌ 差:容易拼写错误
dispatch({ type: 'INCRMENT' }); // 拼写错误,难以发现
2. 使用 action creators
TypeScript
// ✅ 好:使用 action creators 封装 action 创建
const actionCreators = {
addTodo: (text) => ({ type: 'add_todo', payload: { text, id: Date.now() } }),
toggleTodo: (id) => ({ type: 'toggle_todo', payload: { id } }),
deleteTodo: (id) => ({ type: 'delete_todo', payload: { id } }),
};
// 使用
dispatch(actionCreators.addTodo('学习 React'));
// ❌ 差:手动创建 action
dispatch({ type: 'add_todo', payload: { text: '学习 React', id: Date.now() } });
3. 保持 reducer 纯净
TypeScript
// ✅ 好:纯函数 reducer
function reducer(state, action) {
switch (action.type) {
case 'fetch_success':
return { ...state, data: action.payload, loading: false };
default:
return state;
}
}
// ❌ 差:副作用
function reducer(state, action) {
switch (action.type) {
case 'fetch_data':
fetch('/api/data').then(data => {
// 不要在 reducer 中执行副作用!
dispatch({ type: 'fetch_success', payload: data });
});
return state;
default:
return state;
}
}
// ✅ 正确:副作用放在 useEffect 中
useEffect(() => {
let cancelled = false;
fetchData().then(data => {
if (!cancelled) {
dispatch({ type: 'fetch_success', payload: data });
}
});
return () => { cancelled = true; };
}, []);
4. 使用 TypeScript 类型
TypeScript
// 定义 State 和 Action 类型
type State = {
count: number;
};
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset'; payload: number };
// 使用类型
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: action.payload };
default:
return state;
}
}
5. 拆分大型 reducer
TypeScript
// ✅ 好:拆分为多个小 reducer
function userReducer(state, action) {
switch (action.type) {
case 'set_user':
return { ...state, user: action.payload };
default:
return state;
}
}
function todosReducer(state, action) {
switch (action.type) {
case 'add_todo':
return { ...state, todos: [...state.todos, action.payload] };
default:
return state;
}
}
// 使用 combineReducers 组合
function rootReducer(state, action) {
return {
user: userReducer(state.user, action),
todos: todosReducer(state.todos, action),
};
}
常见错误
1. 直接修改 state
TypeScript
// ❌ 错误: 直接修改 state
function reducer(state, action) {
state.todos.push(action.payload); // 直接修改!
return state;
}
// ✅ 正确: 返回新对象
function reducer(state, action) {
return {
...state,
todos: [...state.todos, action.payload],
};
}
2. 在 reducer 中执行副作用
TypeScript
// ❌ 错误: 在 reducer 中调用 API
function reducer(state, action) {
if (action.type === 'fetch') {
fetch('/api/data').then(res => res.json()); // 副作用!
}
return state;
}
// ✅ 正确: 在 useEffect 中调用
useEffect(() => {
const fetchData = async () => {
const data = await fetch('/api/data').then(res => res.json());
dispatch({ type: 'fetch_success', payload: data });
};
fetchData();
}, []);
3. 忘记返回 state
TypeScript
// ❌ 错误: 忘记返回 state
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
// 忘记 default!
}
}
// ✅ 正确: 总是返回 state
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
default:
return state; // 必须有 default
}
}
性能优化
使用 useMemo 缓存计算
TypeScript
import { useMemo } from 'react';
function TodoList() {
const [state, dispatch] = useReducer(todosReducer, initialState);
// 缓存过滤后的 todos,避免每次渲染都重新计算
const filteredTodos = useMemo(() => {
return state.todos.filter(todo => {
if (state.filter === 'active') return !todo.completed;
if (state.filter === 'completed') return todo.completed;
return true;
});
}, [state.todos, state.filter]);
return <ul>{filteredTodos.map(todo => <TodoItem key={todo.id} todo={todo} />)}</ul>;
}
使用 useCallback 稳定 dispatch
TypeScript
// dispatch 引用是稳定的,不需要 useCallback
function TodoItem({ todo }) {
const handleToggle = () => {
dispatch({ type: 'toggle', id: todo.id }); // dispatch 稳定
};
return <button onClick={handleToggle}>{todo.text}</button>;
}
下一步
现在你已经了解了 useReducer,可以继续学习:
这篇文章有帮助吗?
Previous / Next
Related Links