#useReducer 和 useContext 怎么用?适合什么场景?
#一、useReducer 是什么
useReducer 是 useState 的替代方案,适用于包含多个子值、或下一个 state 依赖前一个 state 的复杂状态逻辑。它借鉴了 Redux 的 action/dispatch 模式:
const [state, dispatch] = useReducer(reducer, initialState);#基本用法
const initialState = { count: 0 };
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}`);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>计数:{state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'reset' })}>重置</button>
</div>
);
}#惰性初始化
当初始状态需要通过计算得出时,传入第三个参数 init 函数:
function init(initialCount) {
return { count: initialCount };
}
function Counter({ initialCount }) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
// init 只在首次渲染时执行
}#二、useContext 是什么
useContext 让函数组件能直接读取 Context 的值,替代了过去繁琐的 <Context.Consumer> 模式。
#基本用法
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext('light');
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff',
}}
>
当前主题:{theme}
</button>
);
}#对比 Consumer 写法
// 旧写法:Consumer 嵌套
function OldWay() {
return (
<ThemeContext.Consumer>
{theme => (
<LocaleContext.Consumer>
{locale => (
<UserContext.Consumer>
{user => <div>{theme}-{locale}-{user.name}</div>}
</UserContext.Consumer>
)}
</LocaleContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
// 新写法:useContext 平铺
function NewWay() {
const theme = useContext(ThemeContext);
const locale = useContext(LocaleContext);
const user = useContext(UserContext);
return <div>{theme}-{locale}-{user.name}</div>;
}#三、useReducer + useContext 组合:轻量级状态管理
将两者结合可以实现类似 Redux 的全局状态管理模式,无需引入第三方库:
import { createContext, useContext, useReducer, useMemo } from 'react';
const TodoContext = createContext(null);
const TodoDispatchContext = createContext(null);
function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'TOGGLE':
return state.map(todo =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case 'DELETE':
return state.filter(todo => todo.id !== action.id);
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function TodoProvider({ children }) {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<TodoContext.Provider value={todos}>
<TodoDispatchContext.Provider value={dispatch}>
{children}
</TodoDispatchContext.Provider>
</TodoContext.Provider>
);
}
function useTodos() {
const context = useContext(TodoContext);
if (context === null) throw new Error('useTodos must be within TodoProvider');
return context;
}
function useTodoDispatch() {
const context = useContext(TodoDispatchContext);
if (context === null) throw new Error('useTodoDispatch must be within TodoProvider');
return context;
}使用时各组件各取所需:
function TodoList() {
const todos = useTodos();
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
function TodoItem({ todo }) {
const dispatch = useTodoDispatch();
return (
<li>
<span
onClick={() => dispatch({ type: 'TOGGLE', id: todo.id })}
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'DELETE', id: todo.id })}>删除</button>
</li>
);
}
function AddTodo() {
const dispatch = useTodoDispatch();
const [text, setText] = useState('');
return (
<form onSubmit={e => {
e.preventDefault();
dispatch({ type: 'ADD', text });
setText('');
}}>
<input value={text} onChange={e => setText(e.target.value)} />
<button type="submit">添加</button>
</form>
);
}#四、性能问题与优化
#Context 的重渲染问题
Context 有一个关键特性:当 Provider 的 value 变化时,所有消费该 Context 的组件都会重渲染,无论它们是否使用了变化的那部分数据。
// 问题:只要 count 变化,useTheme 的消费者也会重渲染
function AppProvider({ children }) {
const [theme, setTheme] = useState('light');
const [count, setCount] = useState(0);
return (
<AppContext.Provider value={{ theme, setTheme, count, setCount }}>
{children}
</AppContext.Provider>
);
}#优化策略一:拆分 Context
将不同更新频率的数据拆分到不同的 Context:
function AppProvider({ children }) {
const [theme, setTheme] = useState('light');
const [count, setCount] = useState(0);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<CounterContext.Provider value={{ count, setCount }}>
{children}
</CounterContext.Provider>
</ThemeContext.Provider>
);
}#优化策略二:分离 state 和 dispatch
dispatch 函数是稳定的引用(useReducer 保证),把它放在单独的 Context 中可以避免只需要 dispatch 的组件被 state 变化触发重渲染:
// 上面 TodoProvider 的示例就采用了这个策略
// TodoContext 放 state(变化频繁)
// TodoDispatchContext 放 dispatch(引用稳定)#优化策略三:useMemo 缓存 value
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}#五、useReducer vs useState 怎么选
| 场景 | 推荐 |
|---|---|
| 单一独立状态(开关、计数器) | useState |
| 多个相关联的状态字段 | useReducer |
| 下一个状态依赖复杂逻辑 | useReducer |
| 状态更新逻辑需要被测试 | useReducer(reducer 是纯函数,易测试) |
| 需要传递更新能力给深层组件 | useReducer + useContext |
| 简单的表单(几个字段) | useState |
| 复杂表单(多步骤、校验) | useReducer |
#六、useReducer + useContext vs Redux
| 对比维度 | useReducer + useContext | Redux / RTK |
|---|---|---|
| 安装依赖 | 无 | 需要安装 |
| 学习成本 | 低 | 中等 |
| DevTools | 无 | Redux DevTools |
| 中间件 | 无 | thunk / saga |
| 性能优化 | 需要手动拆分 Context | selector 精确订阅 |
| 适用规模 | 中小型项目 | 中大型项目 |
| 异步处理 | 手动在组件中处理 | 中间件统一处理 |
useReducer + useContext 适合中小型应用或者局部模块的状态管理。当应用规模增大、需要中间件、DevTools 调试、或精细化的性能优化时,Redux Toolkit 是更好的选择。