Skip to content
第 151 / 250 章前端⏱ 10 分钟阅读

第 151 章:Hooks 之 useReducer / useContext

学习目标

  • 掌握 useReducer 管理复杂状态
  • 理解 Context 跨组件共享数据
  • 学会组合 useReducer + Context
  • 替换简单的 Redux

一、useReducer 基础

当 state 复杂(多字段、互相依赖)时,用 useReducer 替代 useState。

1.1 基础形式

tsx
import { useReducer } from 'react';

// ① 定义 reducer
type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset' }
  | { type: 'set'; payload: number };

function reducer(state: { count: number }, action: Action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    case 'reset':     return { count: 0 };
    case 'set':       return { count: action.payload };
    default:          return state;
  }
}

// ② 在组件使用
function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <>
      <p>计数:{state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
      <button onClick={() => dispatch({ type: 'reset' })}>重置</button>
      <button onClick={() => dispatch({ type: 'set', payload: 100 })}>
        设 100
      </button>
    </>
  );
}

1.2 Todo 示例

tsx
interface Todo { id: number; text: string; done: boolean; }
type TodoAction =
  | { type: 'add'; payload: string }
  | { type: 'toggle'; payload: number }
  | { type: 'remove'; payload: number };

function todoReducer(state: Todo[], action: TodoAction): Todo[] {
  switch (action.type) {
    case 'add':
      return [...state, { id: Date.now(), text: action.payload, done: false }];
    case 'toggle':
      return state.map(t => t.id === action.payload ? { ...t, done: !t.done } : t);
    case 'remove':
      return state.filter(t => t.id !== action.payload);
    default:
      return state;
  }
}

function TodoList() {
  const [todos, dispatch] = useReducer(todoReducer, []);

  return (
    <>
      <button onClick={() => dispatch({ type: 'add', payload: '新任务' })}>
        添加
      </button>
      {todos.map(t => (
        <div key={t.id}>
          <input
            type="checkbox"
            checked={t.done}
            onChange={() => dispatch({ type: 'toggle', payload: t.id })}
          />
          {t.text}
          <button onClick={() => dispatch({ type: 'remove', payload: t.id })}>
            ×
          </button>
        </div>
      ))}
    </>
  );
}

二、useContext 跨组件传值

解决 prop drilling 问题。

2.1 创建 Context

tsx
// contexts/ThemeContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';

type Theme = 'light' | 'dark';
interface ThemeContextValue {
  theme: Theme;
  toggle: () => void;
}

const ThemeContext = createContext<ThemeContextValue | null>(null);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>('light');
  const toggle = () => setTheme(t => (t === 'light' ? 'dark' : 'light'));

  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 自定义 hook(强制在 Provider 内使用)
export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme 必须在 ThemeProvider 内使用');
  return ctx;
}

2.2 使用

tsx
// app/layout.tsx
import { ThemeProvider } from '@/contexts/ThemeContext';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

// 任意子组件
'use client';
import { useTheme } from '@/contexts/ThemeContext';

export function ThemeButton() {
  const { theme, toggle } = useTheme();
  return (
    <button onClick={toggle}>
      当前:{theme} - 点击切换
    </button>
  );
}

三、组合:useReducer + Context

构建一个轻量级"全局 store"。

tsx
// contexts/CounterContext.tsx
import { createContext, useContext, useReducer, ReactNode, Dispatch } from 'react';

interface State { count: number; user: User | null; }
type Action =
  | { type: 'inc' }
  | { type: 'setUser'; payload: User };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'inc': return { ...state, count: state.count + 1 };
    case 'setUser': return { ...state, user: action.payload };
    default: return state;
  }
}

interface ContextValue {
  state: State;
  dispatch: Dispatch<Action>;
}

const CounterContext = createContext<ContextValue | null>(null);

export function CounterProvider({ children }: { children: ReactNode }) {
  const [state, dispatch] = useReducer(reducer, { count: 0, user: null });

  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      {children}
    </CounterContext.Provider>
  );
}

export function useCounter() {
  const ctx = useContext(CounterContext);
  if (!ctx) throw new Error('useCounter 必须在 CounterProvider 内');
  return ctx;
}
tsx
// 使用
'use client';
import { useCounter } from '@/contexts/CounterContext';

export function Counter() {
  const { state, dispatch } = useCounter();
  return (
    <>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: 'inc' })}>+1</button>
    </>
  );
}

四、性能优化

4.1 拆分 Context

tsx
// ❌ 一个 Context 包含所有数据
const AppContext = createContext({ user, theme, count, todos });

// ✅ 拆分成多个
<UserContext.Provider>
  <ThemeContext.Provider>
    <CounterContext.Provider>
      {children}
    </CounterContext.Provider>
  </ThemeContext.Provider>
</UserContext.Provider>

4.2 拆分 value

tsx
// ❌ value 每次是新对象
<MyContext.Provider value={{ user, setUser }}>
  {children}
</MyContext.Provider>

// ✅ 用 useMemo
const value = useMemo(() => ({ user, setUser }), [user]);
<MyContext.Provider value={value}>
  {children}
</MyContext.Provider>

五、什么时候用 useContext vs Zustand

场景推荐
简单全局状态(主题、用户)useContext
中等复杂度(多组件共享 + 更新频繁)Zustand
复杂业务状态 + 时间旅行Redux Toolkit
简单局部状态useState/useReducer

六、本章小结

Hook作用
useReducer复杂状态的统一管理
useContext跨组件共享数据
组合替代简单 Redux

动手练习

  1. 用 useReducer 实现 Todo 列表
  2. 创建 ThemeContext,实现主题切换
  3. 创建 AuthContext,管理登录状态
  4. 组合 useReducer + Context,做一个小购物车

推荐阅读


下一章:第 152 章:自定义 Hooks

本站基于 VitePress 构建 · 由 Codebook 团队维护