第 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 |
动手练习
- 用 useReducer 实现 Todo 列表
- 创建 ThemeContext,实现主题切换
- 创建 AuthContext,管理登录状态
- 组合 useReducer + Context,做一个小购物车
推荐阅读
下一章:第 152 章:自定义 Hooks →