React useReducer 高级模式
useReducer 基础回顾
useReducer 是 useState 的替代方案,适用于复杂状态逻辑:
const [state, dispatch] = useReducer(reducer, initialState);
// reducer 函数签名
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
}
选择 useReducer 而不是 useState 的标准:
- 状态变化有多个互斥的逻辑分支
- 状态变化相互依赖,需要基于旧状态计算
- 状态是复杂对象,多个属性需要联动更新
复杂状态形状的设计
扁平化 vs 嵌套
// 不好的设计:嵌套过深
const state = {
user: {
profile: {
name: 'Alice',
settings: {
theme: 'dark',
notifications: {
email: true,
push: false
}
}
}
}
};
// 扁平化设计:更容易单独更新
const state = {
user: {
id: '1',
name: 'Alice',
},
settings: {
theme: 'dark',
notifications: {
email: true,
push: false
}
}
};
扁平化设计的优势在于更新更简单,不需要深层解构:
// 嵌套更新:容易出错
return {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
settings: {
...state.user.profile.settings,
notifications: {
...state.user.profile.settings.notifications,
push: true
}
}
}
}
};
// 扁平化更新:简单直接
return {
...state,
settings: {
...state.settings,
notifications: {
...state.settings.notifications,
push: true
}
}
};
使用 Immer 简化不可变更新
Immer 是一个让你用可变语法写不可变更新的库:
import { produce } from 'immer';
const reducer = (state, action) => {
switch (action.type) {
case 'UPDATE_NOTIFICATION':
return produce(state, draft => {
// 可以直接修改 draft
draft.settings.notifications.push = action.value;
});
case 'ADD_USER':
return produce(state, draft => {
draft.users.push(action.user);
});
default:
return state;
}
};
Reducer 组合模式
分离 Reducer
当状态很复杂时,可以拆分成多个 reducer:
// 初始状态
const initialState = {
users: {
list: [],
loading: false,
error: null
},
posts: {
list: [],
loading: false,
error: null
}
};
// 拆分的 reducer
function usersReducer(state, action) {
switch (action.type) {
case 'USERS_LOADING':
return { ...state, loading: true };
case 'USERS_SUCCESS':
return { loading: false, list: action.users };
case 'USERS_ERROR':
return { loading: false, error: action.error };
default:
return state;
}
}
function postsReducer(state, action) {
// ...
}
// 组合 reducer
function combinedReducer(state, action) {
return {
users: usersReducer(state.users, action),
posts: postsReducer(state.posts, action)
};
}
function App() {
const [state, dispatch] = useReducer(combinedReducer, initialState);
}
使用 combineReducers( Redux 风格)
import { combineReducers } from 'redux'; // 或者自己实现
const usersReducer = (state = { list: [] }, action) => {
switch (action.type) {
case 'ADD_USER':
return { ...state, list: [...state.list, action.user] };
default:
return state;
}
};
const postsReducer = (state = { list: [] }, action) => {
switch (action.type) {
case 'ADD_POST':
return { ...state, list: [...state.list, action.post] };
default:
return state;
}
};
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer
});
Action Creator 模式
基础 Action Creator
// 简单的 action creator
const addTodo = (text) => ({
type: 'ADD_TODO',
payload: { id: Date.now(), text, done: false }
});
const toggleTodo = (id) => ({
type: 'TOGGLE_TODO',
payload: { id }
});
const deleteTodo = (id) => ({
type: 'DELETE_TODO',
payload: { id }
});
// 使用
dispatch(addTodo('Learn React'));
dispatch(toggleTodo(1));
dispatch(deleteTodo(1));
带验证的 Action Creator
const addTodo = (text) => {
if (!text || typeof text !== 'string') {
return { type: 'ERROR', payload: 'Invalid text' };
}
if (text.length > 200) {
return { type: 'ERROR', payload: 'Text too long' };
}
return {
type: 'ADD_TODO',
payload: { id: Date.now(), text: text.trim(), done: false }
};
};
Thunk 模式(异步 Action)
// 异步 action creator(thunk)
const fetchUser = (userId) => {
return async (dispatch, getState) => {
dispatch({ type: 'FETCH_USER_REQUEST' });
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
} catch (error) {
dispatch({ type: 'FETCH_USER_ERROR', payload: error.message });
}
};
};
// 使用
dispatch(fetchUser(123));
Context 配合 Reducer
基础模式
const StateContext = createContext(null);
const DispatchContext = createContext(null);
function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
// 使用
function ConsumerComponent() {
const state = useContext(StateContext);
const dispatch = useContext(DispatchContext);
// 或者用自定义 hook 简化
// const { state, dispatch } = useAppContext();
}
分离 State 和 Dispatch
最佳实践是分离 state 和 dispatch 的 Context:
const StateContext = createContext(null);
const DispatchContext = createContext(null);
// 分离的好处:
// 1. dispatch 函数永远稳定,不需要是 Context 的值
// 2. 可以单独订阅 state 的特定部分
// 3. 更容易进行性能优化
带选择的 Context 消费
function UserProfile() {
// 只订阅 user 部分,不关心其他 state
const user = useContextSelector(StateContext, s => s.user);
// 或
const { user } = useAppState();
}
状态范式化
范式化数据结构
范式化(Normalizing)是数据库概念,指将嵌套数据拆分成表/实体:
// 非范式化
const state = {
posts: [
{
id: 1,
title: 'Hello',
author: { id: 1, name: 'Alice' },
comments: [
{ id: 1, author: { id: 2, name: 'Bob' }, text: 'Great!' }
]
}
]
};
// 范式化
const state = {
posts: {
byId: {
1: { id: 1, title: 'Hello', authorId: 1, commentIds: [1] }
},
allIds: [1]
},
users: {
byId: {
1: { id: 1, name: 'Alice' },
2: { id: 2, name: 'Bob' }
},
allIds: [1, 2]
},
comments: {
byId: {
1: { id: 1, authorId: 2, text: 'Great!' }
},
allIds: [1]
}
};
范式化的优势
- 更新简单:修改用户信息只需要更新一个地方
- 查找高效:不需要遍历数组
- 避免重复:数据只存储一份
创建 Selector
// 获取单个 post
const selectPostById = (state, postId) => state.posts.byId[postId];
// 获取带展开作者的 post
const selectPostWithAuthor = (state, postId) => {
const post = state.posts.byId[postId];
if (!post) return null;
const author = state.users.byId[post.authorId];
return { ...post, author };
};
// 获取所有 posts
const selectAllPosts = (state) =>
state.posts.allIds.map(id => selectPostById(state, id));
// 使用
function PostList() {
const posts = useAppState(state => selectAllPosts(state));
// 或
const post = useAppState(state => selectPostById(state, 123));
}
useReducer 与 useState 的选择
useReducer 的优势场景
- 复杂的状态变化逻辑:多个 action 类型,相互依赖
- 状态变化需要触发副作用:如日志记录、持久化
- 状态变化需要被撤销/重做:reducer 可以记录历史
- 组件树中多个组件需要访问状态:配合 Context
useState 的优势场景
- 简单、独立的状态:布尔值、字符串、数字
- 状态变化简单直接:直接赋值,不需要计算
- 状态之间没有关联:一个状态变化不影响另一个
混用模式
function ComplexComponent() {
// useState 用于简单状态
const [isExpanded, setIsExpanded] = useState(false);
// useReducer 用于复杂状态
const [formState, dispatch] = useReducer(formReducer, initialFormState);
return (
<div>
<button onClick={() => setIsExpanded(!isExpanded)}>Toggle</button>
{isExpanded && <Form state={formState} dispatch={dispatch} />}
</div>
);
}
面试中的表达
面试中聊到 useReducer,通常是在考察你对复杂状态管理的理解:
useReducer 本质上是把状态变化的逻辑集中到一个纯函数(reducer)中,通过 action 来描述「发生了什么」,而不是直接修改状态。这种模式的好处是:所有状态变化都是可预测的,调试容易,也方便实现撤销/重做。
Reducer 组合和 Context 配合是 React 状态管理的常见模式。拆分 reducer 可以让代码更模块化,配合 Context 可以实现类似 Redux 的全局状态管理,但更轻量。
关于 useReducer vs useState,我的经验是:简单状态用 useState,复杂状态用 useReducer。当状态变化逻辑开始变得复杂,或者需要撤销/重做功能时,就该考虑 useReducer 了。
延展阅读
- React Docs: useReducer — 官方 useReducer 文档
- React Docs: Scaling Up with Reducer and Context — Reducer 与 Context 结合
- Redux: Reducers — Redux Reducer 设计原则
- Immer: Immutability — 不可变数据更新模式