useReducer
通过 reducer 函数管理复杂组件状态
核心概述
当组件状态更新逻辑变得复杂时,使用 useState 会导致代码难以维护。 例如,一个状态对象的多个字段需要联动更新,或者下一个状态依赖于前一个状态。 在这种场景下,状态更新逻辑散落在各个事件处理程序中,既难以复用,也难以测试。
useReducer 通过引入 reducer 模式 来解决这个痛点。 它将所有状态更新逻辑集中到一个纯函数中,通过 dispatch(action) 触发状态转换。 这种方式让状态变化变得可预测、可测试,并且方便在多个组件间共享状态逻辑。
useReducer 适用于以下场景:
- 状态结构复杂(对象或数组),且多个子值需要联动更新
- 下一个状态依赖于前一个状态(如计数器、待办事项列表)
- 状态更新逻辑复杂,包含多个子步骤
- 需要测试状态更新逻辑(纯函数便于单元测试)
- 作为 useState 的替代方案,配合 Context 实现跨组件状态共享
💡 心智模型:状态机
将 useReducer 想象成一个状态机:
- state: 当前所处的状态
- action: 触发状态转换的事件
- reducer: 状态转换规则(给定当前状态和事件,计算下一个状态)
- dispatch: 发送事件的按钮
与传统状态管理不同,你不能直接"跳转"到新状态,而是通过发送 action 来触发状态转换。 这种间接性让所有状态变化都被记录和追踪,便于调试和测试。
技术规格
类型签名
function useReducer<R extends Reducer<any, any>, I>(
reducer: R,
initialArg: I,
init?: (arg: I) => R extends Reducer<any, infer S> ? S : never
): [R extends Reducer<any, infer S> ? S : never, Dispatch<ReducerAction<R>>];
type Reducer<S, A> = (state: S, action: A) => S;
type Dispatch<A> = (action: A) => void;参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
reducer | (state, action) => newState | 纯函数,接收当前状态和 action,返回新状态 |
initialArg | any | 初始状态值,或传给 init 函数的参数 |
init | (arg) => initialState | 可选的惰性初始化函数,仅在初始渲染时调用一次 |
返回值
返回一个包含两个值的数组:
- state: 当前的状态值
- dispatch: 触发状态更新的函数,接收一个 action 对象
运行机制
渲染时机:
- 调用
dispatch(action)会通知 React 在下次渲染时使用reducer(currentState, action)的返回值作为新状态 - React 会批处理多次 dispatch 调用,确保在一次渲染周期内只更新一次状态
- dispatch 函数引用在组件生命周期内保持稳定,可以作为 useEffect 的依赖
与 Redux 的区别:
- React 的 useReducer 不需要配置 store 和中间件
- 不支持订阅多个 reducer 的组合(需手动实现)
- dispatch 是同步的,但状态更新遵循 React 的批处理规则
实战演练
基础用法:计数器
import { useReducer } from 'react';
// 1. 定义 Action 类型(建议使用 TypeScript)
type CounterAction =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset'; payload: number };
// 2. 定义 State 类型
type CounterState = {
count: number;
};
// 3. 定义 reducer 纯函数
function counterReducer(state: CounterState, action: CounterAction): CounterState {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: action.payload };
default:
// TypeScript 的类型守卫,确保处理所有 action 类型
const exhaustiveCheck: never = action;
return state;
}
}
// 4. 使用 useReducer
function Counter({ initialCount = 0 }: { initialCount?: number }) {
const [state, dispatch] = useReducer(counterReducer, { count: initialCount });
return (
<div>
<p>当前计数: {state.count}</p>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>
重置
</button>
</div>
);
}生产级案例:表单状态管理
展示如何使用 useReducer 管理复杂表单状态,包括验证、异步提交和错误处理。
import { useReducer, useState, useCallback, FormEvent } from 'react';
// ============= 类型定义 =============
interface FormData {
username: string;
email: string;
password: string;
}
interface FormErrors {
username?: string;
email?: string;
password?: string;
_form?: string; // 全局错误(如网络错误)
}
interface FormState {
data: FormData;
errors: FormErrors;
touched: Set<string>; // 使用 Set 追踪已修改的字段
isSubmitting: boolean;
submitSuccess: boolean;
}
type FormAction =
| { type: 'FIELD_CHANGE'; field: keyof FormData; value: string }
| { type: 'FIELD_TOUCH'; field: string }
| { type: 'SET_ERROR'; field: keyof FormErrors; error: string | undefined }
| { type: 'SUBMIT_START' }
| { type: 'SUBMIT_SUCCESS' }
| { type: 'SUBMIT_ERROR'; error: string }
| { type: 'RESET_FORM' };
// ============= 初始状态 =============
const initialFormData: FormData = {
username: '',
email: '',
password: '',
};
const initialState: FormState = {
data: initialFormData,
errors: {},
touched: new Set<string>(),
isSubmitting: false,
submitSuccess: false,
};
// ============= Reducer =============
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'FIELD_CHANGE': {
const newData = { ...state.data, [action.field]: action.value };
const newErrors = { ...state.errors };
// 实时验证:清空当前字段的错误(如果用户正在修改)
if (state.touched.has(action.field)) {
newErrors[action.field] = validateField(action.field, action.value);
}
return {
...state,
data: newData,
errors: newErrors,
};
}
case 'FIELD_TOUCH': {
const newTouched = new Set(state.touched);
newTouched.add(action.field);
// 触发验证
const fieldError = validateField(
action.field as keyof FormData,
state.data[action.field as keyof FormData]
);
return {
...state,
touched: newTouched,
errors: { ...state.errors, [action.field]: fieldError },
};
}
case 'SET_ERROR':
return {
...state,
errors: { ...state.errors, [action.field]: action.error },
};
case 'SUBMIT_START':
return {
...state,
isSubmitting: true,
errors: { _form: undefined },
};
case 'SUBMIT_SUCCESS':
return {
...state,
isSubmitting: false,
submitSuccess: true,
errors: {},
};
case 'SUBMIT_ERROR':
return {
...state,
isSubmitting: false,
errors: { _form: action.error },
};
case 'RESET_FORM':
return initialState;
default:
return state;
}
}
// ============= 验证函数 =============
function validateField(field: keyof FormData, value: string): string | undefined {
switch (field) {
case 'username':
if (!value) return '用户名不能为空';
if (value.length < 3) return '用户名至少 3 个字符';
if (value.length > 20) return '用户名最多 20 个字符';
return undefined;
case 'email':
if (!value) return '邮箱不能为空';
const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
if (!emailRegex.test(value)) return '邮箱格式不正确';
return undefined;
case 'password':
if (!value) return '密码不能为空';
if (value.length < 8) return '密码至少 8 个字符';
return undefined;
default:
return undefined;
}
}
function validateForm(data: FormData): FormErrors {
const errors: FormErrors = {};
Object.keys(data).forEach((key) => {
const error = validateField(key as keyof FormData, data[key as keyof FormData]);
if (error) {
errors[key as keyof FormErrors] = error;
}
});
return errors;
}
// ============= 组件 =============
export function RegistrationForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
// 处理字段变化
const handleChange = useCallback((field: keyof FormData) => (
e: React.ChangeEvent<HTMLInputElement>
) => {
dispatch({
type: 'FIELD_CHANGE',
field,
value: e.target.value,
});
}, []);
// 处理字段失焦(触发验证)
const handleBlur = useCallback((field: string) => () => {
dispatch({ type: 'FIELD_TOUCH', field });
}, []);
// 处理表单提交
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
// 1. 验证所有字段
const errors = validateForm(state.data);
const hasErrors = Object.values(errors).some(error => error !== undefined);
if (hasErrors) {
// 标记所有字段为 touched,显示所有错误
Object.keys(state.data).forEach((field) => {
dispatch({ type: 'FIELD_TOUCH', field });
});
return;
}
// 2. 开始提交
dispatch({ type: 'SUBMIT_START' });
try {
// 模拟 API 调用
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(state.data),
});
if (!response.ok) {
throw new Error('注册失败,请稍后重试');
}
dispatch({ type: 'SUBMIT_SUCCESS' });
// 3 秒后重置表单
setTimeout(() => {
dispatch({ type: 'RESET_FORM' });
}, 3000);
} catch (error) {
dispatch({
type: 'SUBMIT_ERROR',
error: error instanceof Error ? error.message : '未知错误',
});
}
};
return (
<form onSubmit={handleSubmit} className="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
<h2 className="text-2xl font-bold mb-6">注册账号</h2>
{/* 全局错误 */}
{state.errors._form && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-600 rounded">
{state.errors._form}
</div>
)}
{/* 提交成功 */}
{state.submitSuccess && (
<div className="mb-4 p-3 bg-green-50 border border-green-200 text-green-600 rounded">
注册成功!
</div>
)}
{/* 用户名 */}
<div className="mb-4">
<label className="block text-sm font-medium mb-1">用户名</label>
<input
type="text"
value={state.data.username}
onChange={handleChange('username')}
onBlur={handleBlur('username')}
className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{state.touched.has('username') && state.errors.username && (
<p className="mt-1 text-sm text-red-500">{state.errors.username}</p>
)}
</div>
{/* 邮箱 */}
<div className="mb-4">
<label className="block text-sm font-medium mb-1">邮箱</label>
<input
type="email"
value={state.data.email}
onChange={handleChange('email')}
onBlur={handleBlur('email')}
className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{state.touched.has('email') && state.errors.email && (
<p className="mt-1 text-sm text-red-500">{state.errors.email}</p>
)}
</div>
{/* 密码 */}
<div className="mb-6">
<label className="block text-sm font-medium mb-1">密码</label>
<input
type="password"
value={state.data.password}
onChange={handleChange('password')}
onBlur={handleBlur('password')}
className="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{state.touched.has('password') && state.errors.password && (
<p className="mt-1 text-sm text-red-500">{state.errors.password}</p>
)}
</div>
{/* 提交按钮 */}
<button
type="submit"
disabled={state.isSubmitting}
className="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
>
{state.isSubmitting ? '提交中...' : '注册'}
</button>
</form>
);
}关键点:这个示例展示了 useReducer 的核心优势:
- 所有状态逻辑集中在一个 reducer 中,易于维护和测试
- 表单验证逻辑与 UI 组件分离
- dispatch 函数稳定,可作为 useCallback 的依赖
- 类型安全的 action(使用 TypeScript discriminated unions)
生产级案例:购物车(多状态联动)
展示如何处理多个状态字段联动更新,避免使用 useEffect。
import { useReducer, useCallback, useMemo } from 'react';
// ============= 类型定义 =============
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartState {
items: CartItem[];
totalItems: number;
totalPrice: number;
discount: number;
finalPrice: number;
}
type CartAction =
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'SET_DISCOUNT'; payload: number }
| { type: 'CLEAR_CART' };
// ============= Reducer =============
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM': {
const existingItem = state.items.find(item => item.id === action.payload.id);
let newItems: CartItem[];
if (existingItem) {
// 商品已存在,增加数量
newItems = state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
// 新商品,添加到购物车
newItems = [...state.items, { ...action.payload, quantity: 1 }];
}
// 计算新总价(在 reducer 中一次性完成,避免 useEffect)
const totalItems = newItems.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = newItems.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const finalPrice = totalPrice * (1 - state.discount / 100);
return {
items: newItems,
totalItems,
totalPrice,
discount: state.discount,
finalPrice,
};
}
case 'REMOVE_ITEM': {
const newItems = state.items.filter(item => item.id !== action.payload);
const totalItems = newItems.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = newItems.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const finalPrice = totalPrice * (1 - state.discount / 100);
return {
items: newItems,
totalItems,
totalPrice,
discount: state.discount,
finalPrice,
};
}
case 'UPDATE_QUANTITY': {
const newItems = state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
);
const totalItems = newItems.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = newItems.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const finalPrice = totalPrice * (1 - state.discount / 100);
return {
items: newItems,
totalItems,
totalPrice,
discount: state.discount,
finalPrice,
};
}
case 'SET_DISCOUNT': {
const finalPrice = state.totalPrice * (1 - action.payload / 100);
return {
...state,
discount: action.payload,
finalPrice,
};
}
case 'CLEAR_CART':
return {
items: [],
totalItems: 0,
totalPrice: 0,
discount: 0,
finalPrice: 0,
};
default:
return state;
}
}
const initialState: CartState = {
items: [],
totalItems: 0,
totalPrice: 0,
discount: 0,
finalPrice: 0,
};
// ============= 组件 =============
export function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
// 添加商品
const handleAddItem = useCallback((item: Omit<CartItem, 'quantity'>) => {
dispatch({ type: 'ADD_ITEM', payload: item });
}, []);
// 删除商品
const handleRemoveItem = useCallback((id: string) => {
dispatch({ type: 'REMOVE_ITEM', payload: id });
}, []);
// 更新数量
const handleUpdateQuantity = useCallback((id: string, quantity: number) => {
if (quantity <= 0) {
dispatch({ type: 'REMOVE_ITEM', payload: id });
} else {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
}
}, []);
// 设置折扣
const handleSetDiscount = useCallback((discount: number) => {
dispatch({ type: 'SET_DISCOUNT', payload: Math.max(0, Math.min(100, discount)) });
}, []);
// 清空购物车
const handleClearCart = useCallback(() => {
dispatch({ type: 'CLEAR_CART' });
}, []);
return (
<div className="max-w-4xl mx-auto p-6 bg-white rounded-lg shadow">
<h2 className="text-2xl font-bold mb-6">购物车</h2>
{/* 购物车商品列表 */}
{state.items.length === 0 ? (
<p className="text-center text-gray-500 py-8">购物车是空的</p>
) : (
<div className="space-y-4 mb-6">
{state.items.map(item => (
<div
key={item.id}
className="flex items-center justify-between p-4 border rounded"
>
<div>
<h3 className="font-semibold">{item.name}</h3>
<p className="text-gray-600">单价: ¥{item.price}</p>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<button
onClick={() => handleUpdateQuantity(item.id, item.quantity - 1)}
className="px-2 py-1 border rounded"
>
-
</button>
<span className="w-8 text-center">{item.quantity}</span>
<button
onClick={() => handleUpdateQuantity(item.id, item.quantity + 1)}
className="px-2 py-1 border rounded"
>
+
</button>
</div>
<p className="font-semibold w-24 text-right">
¥{item.price * item.quantity}
</p>
<button
onClick={() => handleRemoveItem(item.id)}
className="text-red-500 hover:text-red-700"
>
删除
</button>
</div>
</div>
))}
</div>
)}
{/* 价格汇总 */}
{state.items.length > 0 && (
<div className="border-t pt-4 space-y-2">
<div className="flex justify-between">
<span>商品总数:</span>
<span>{state.totalItems} 件</span>
</div>
<div className="flex justify-between">
<span>总价格:</span>
<span>¥{state.totalPrice.toFixed(2)}</span>
</div>
{/* 折扣设置 */}
<div className="flex items-center justify-between">
<span>折扣(%):</span>
<input
type="number"
min="0"
max="100"
value={state.discount}
onChange={(e) => handleSetDiscount(Number(e.target.value))}
className="w-20 px-2 py-1 border rounded text-right"
/>
</div>
<div className="flex justify-between text-lg font-bold border-t pt-2">
<span>最终价格:</span>
<span className="text-green-600">¥{state.finalPrice.toFixed(2)}</span>
</div>
<button
onClick={handleClearCart}
className="w-full mt-4 bg-red-500 text-white py-2 rounded hover:bg-red-600"
>
清空购物车
</button>
</div>
)}
{/* 演示:快速添加商品 */}
<div className="mt-8 pt-6 border-t">
<h3 className="font-semibold mb-4">快速添加商品</h3>
<div className="flex gap-2">
<button
onClick={() => handleAddItem({ id: '1', name: 'React 书籍', price: 59.9 })}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
添加 React 书籍
</button>
<button
onClick={() => handleAddItem({ id: '2', name: 'TypeScript 入门', price: 39.9 })}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
>
添加 TypeScript 入门
</button>
<button
onClick={() => handleAddItem({ id: '3', name: 'Next.js 实战', price: 79.9 })}
className="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600"
>
添加 Next.js 实战
</button>
</div>
</div>
</div>
);
}优势:这种方式避免了使用 useEffect 来计算派生状态(totalItems, totalPrice)。 所有状态更新在 reducer 中一次性完成,代码更简洁,性能更好。
避坑指南
❌ 陷阱 1: 直接修改 State
Reducer 必须是纯函数,不能直接修改 state 对象。直接修改会导致:
- React 无法检测到状态变化,组件不会重新渲染
- 破坏状态的不可变性,导致难以追踪的状态变化
- 可能引发内存泄漏和性能问题
❌ 错误代码
function reducer(state: State, action: Action) {
switch (action.type) {
case 'add_item':
// ❌ 直接修改 state!
state.items.push(action.payload);
return state;
case 'update_user':
// ❌ 直接修改嵌套对象!
state.user.name = action.payload.name;
return state;
case 'remove_item':
// ❌ 直接修改数组!
state.items.splice(action.index, 1);
return state;
}
}✅ 修正代码
function reducer(state: State, action: Action) {
switch (action.type) {
case 'add_item':
// ✅ 返回新对象和新数组
return {
...state,
items: [...state.items, action.payload],
};
case 'update_user':
return {
...state,
user: { ...state.user, name: action.payload.name },
};
case 'remove_item':
return {
...state,
items: state.items.filter((_, i) => i !== action.index),
};
}
}心智模型:将 state 视为只读的。每次状态更新都创建一个全新的对象,而不是在原对象上"打补丁"。 这让状态变化可预测,便于时间旅行调试和状态追踪。
❌ 陷阱 2: 在 Reducer 中执行副作用
Reducer 应该是纯函数,执行副作用会破坏其可预测性和可测试性:
- API 调用、setTimeout、console.log 都是副作用
- 副作用应该在 useEffect 中处理,通过 dispatch 触发 reducer
- 在 reducer 中执行副作用会导致不可预测的行为和难以复现的 bug
❌ 错误代码
function reducer(state, action) {
switch (action.type) {
case 'fetch_user':
// ❌ 在 reducer 中调用 API!
fetch('/api/users')
.then(res => res.json())
.then(data => {
// 这里 dispatch 不会生效
return { type: 'fetch_success', payload: data };
});
return { ...state, loading: true };
case 'log_action':
// ❌ 在 reducer 中打印日志!
console.log('Action:', action);
// ❌ 在 reducer 中修改全局变量!
window.analytics.track('action', action);
return state;
}
}✅ 修正代码
function Component() {
const [state, dispatch] = useReducer(reducer, initialState);
// ✅ 在 useEffect 中处理副作用
useEffect(() => {
if (state.shouldFetch) {
const controller = new AbortController();
fetch('/api/users', { signal: controller.signal })
.then(res => res.json())
.then(data => {
dispatch({ type: 'fetch_success', payload: data });
})
.catch(error => {
dispatch({ type: 'fetch_error', payload: error.message });
});
return () => controller.abort();
}
}, [state.shouldFetch]);
// ✅ 在组件中处理日志
const handleAction = (action) => {
console.log('Dispatching:', action);
window.analytics.track('action', action);
dispatch(action);
};
}❌ 陷阱 3: 忘记处理所有 Action 类型
在 TypeScript 中,如果没有在 default 分支中处理未知 action,可能导致运行时错误:
❌ 错误代码
type Action =
| { type: 'increment' }
| { type: 'decrement' };
function reducer(state, action: Action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
// ❌ 没有 default 分支!
// TypeScript 不会检查遗漏的 action 类型
}
}
// 如果以后添加新 action:
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' }; // 新增,但忘记在 reducer 中处理✅ 修正代码
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: 0 };
default:
// ✅ 使用 exhaustiveness check
const exhaustiveCheck: never = action;
// TypeScript 会在编译时报错:
// "Type 'string' is not assignable to type 'never'"
return state;
}
}
// 或者使用 @typescript-eslint 的规则:
// eslint-disable-next-line @typescript-eslint-switch-exhaustiveness-check
}推荐工具:启用 ESLint 规则 @typescript-eslint/switch-exhaustiveness-check, 它会在 switch 语句未处理所有情况时自动报错。
❌ 陷阱 4: 过度使用 useReducer
并不是所有状态都需要 useReducer,过度使用会导致代码冗长和难以维护:
❌ 错误代码
// ❌ 简单状态不需要 useReducer
type Action = { type: 'set_value'; payload: string };
function simpleReducer(state: string, action: Action) {
switch (action.type) {
case 'set_value':
return action.payload;
}
}
function Input() {
const [value, dispatch] = useReducer(simpleReducer, '');
return (
<input
value={value}
onChange={(e) => dispatch({
type: 'set_value',
payload: e.target.value,
})}
/>
);
}✅ 修正代码
// ✅ 简单状态使用 useState
function Input() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
// ✅ 复杂状态才使用 useReducer
type FormAction =
| { type: 'SET_FIELD'; field: string; value: string }
| { type: 'SUBMIT' }
| { type: 'RESET' };
function ComplexForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
// ...
}最佳实践
✅ 1. 使用 TypeScript Discriminated Unions
使用可辨识联合类型(Discriminated Unions)来约束 action,确保类型安全:
// ✅ 推荐:使用 type 而不是 enum
type CounterAction =
| { type: 'increment'; by: number }
| { type: 'decrement'; by: number }
| { type: 'reset'; to: number };
// TypeScript 可以自动推导 action 的类型
function reducer(state: State, action: CounterAction): State {
switch (action.type) {
case 'increment':
// ✅ TypeScript 知道 action.by 存在
return { count: state.count + action.by };
case 'decrement':
return { count: state.count - action.by };
case 'reset':
// ✅ TypeScript 知道 action.to 存在
return { count: action.to };
default:
return state;
}
}
// ✅ dispatch 时有类型提示
dispatch({ type: 'increment', by: 1 });✅ 2. 分离 Reducer 和组件逻辑
将 reducer 定义在组件外部,便于测试和复用:
// ✅ 将 reducer 定义在单独的文件中
// src/reducers/cartReducer.ts
export function cartReducer(state: CartState, action: CartAction): CartState {
// ...
}
// src/components/Cart.tsx
import { cartReducer } from '@/reducers/cartReducer';
function Cart() {
const [state, dispatch] = useReducer(cartReducer, initialCartState);
// ...
}
// ✅ 可以轻松测试 reducer
import { cartReducer } from './cartReducer';
describe('cartReducer', () => {
it('should add item to cart', () => {
const state = { items: [] };
const action = { type: 'ADD_ITEM', payload: { id: '1', name: 'Item' } };
const newState = cartReducer(state, action);
expect(newState.items).toHaveLength(1);
});
});✅ 3. 结合 Context 实现全局状态
使用 useReducer + Context 模式实现轻量级状态管理:
// context/AppContext.tsx
import { createContext, useContext, useReducer } from 'react';
type AppState = {
user: User | null;
theme: 'light' | 'dark';
};
type AppAction =
| { type: 'SET_USER'; payload: User | null }
| { type: 'SET_THEME'; payload: 'light' | 'dark' };
const AppContext = createContext<{
state: AppState;
dispatch: Dispatch<AppAction>;
} | null>(null);
function appReducer(state: AppState, action: AppAction): AppState {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'SET_THEME':
return { ...state, theme: action.payload };
default:
return state;
}
}
export function AppProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(appReducer, {
user: null,
theme: 'light',
});
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
}
export function useAppState() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useAppState must be used within AppProvider');
}
return context;
}
// 组件中使用
function UserProfile() {
const { state, dispatch } = useAppState();
return (
<div>
<p>Welcome, {state.user?.name}</p>
<button onClick={() => dispatch({ type: 'SET_USER', payload: null })}>
Logout
</button>
</div>
);
}✅ 4. 使用 Immer 简化复杂状态更新
对于深层嵌套的状态,使用 Immer 可以让你写"可变"风格的代码,但保持不可变性:
import { useImmerReducer } from 'use-immer';
// ❌ 不使用 Immer:深层更新很繁琐
function reducer(state, action) {
switch (action.type) {
case 'update_city':
return {
...state,
user: {
...state.user,
address: {
...state.user.address,
city: action.payload,
},
},
};
}
}
// ✅ 使用 Immer:像修改对象一样写代码
function reducer(draft, action) {
switch (action.type) {
case 'update_city':
draft.user.address.city = action.payload; // 直接修改!
break;
}
}
function Component() {
const [state, dispatch] = useImmerReducer(reducer, initialState);
// ...
}