Hook16.8+

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,返回新状态
initialArgany初始状态值,或传给 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);
  // ...
}

延伸阅读