状态管理
当应用状态跨多个组件共享且层级复杂时,需要状态管理方案。本章由简到繁介绍三种模式:Context + useReducer、Zustand(轻量)、Redux Toolkit(重量)。
Vue 用户对照:Context ≈ provide/inject,Zustand ≈ Pinia。
方案选型速查
| 方案 | 学习曲线 | 包体积 | 适用规模 | DevTools |
|---|---|---|---|---|
| Context + useReducer | 低 | 0 | 小~中 | ❌ |
| Zustand | 低 | ~2KB | 小~大 | ✅ (Redux DevTools) |
| Jotai | 低 | ~3KB | 小~大 | ✅ |
| Redux Toolkit | 中 | ~11KB | 中~大 | ✅ (最完善) |
| MobX | 中 | ~16KB | 中~大 | ✅ |
💡 最佳实践:新项目默认选择 Zustand。它 API 极简、TypeScript 友好、性能好。只有团队历史原因或极度复杂的表单场景才考虑 Redux Toolkit。
方案一:Context + useReducer
适合小到中型应用,无需引入第三方库。
// store.js
import { createContext, useContext, useReducer } from 'react';
const StoreContext = createContext(null);
const DispatchContext = createContext(null);
const initialState = { todos: [], filter: 'all' };
function reducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return { ...state, todos: [...state.todos, action.payload] };
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(t =>
t.id === action.payload ? { ...t, done: !t.done } : t
),
};
case 'SET_FILTER':
return { ...state, filter: action.payload };
default:
return state;
}
}
export function StoreProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StoreContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StoreContext.Provider>
);
}
export function useStore() {
return useContext(StoreContext);
}
export function useDispatch() {
return useContext(DispatchContext);
}🚨 陷阱:Context 有一个致命问题——任何一个 Context value 变化,所有消费该 Context 的组件都会重渲染。将 state 和 dispatch 分别放两个 Context(如上所示),只消费 dispatch 的组件不会因 state 变化而重渲染。
Context + useReducer 的痛点
一个 Context value 变化
↓
所有 useContext 的组件重渲染
↓
即使该组件只用到了 value 中未变化的部分解决方案:
- 拆分 Context:按功能域拆分多个 Context(
ThemeContext、UserContext、CartContext) - 选择器 + 订阅:手动实现或用第三方库(use-context-selector)
方案二:Zustand(推荐)
npm install zustand基础用法
import { create } from 'zustand';
const useBearStore = create((set, get) => ({
bears: 0,
fishes: 0,
increaseBears: () => set(state => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
// get() 读取当前状态,无需作为依赖
report: () => console.log(`Bears: ${get().bears}, Fishes: ${get().fishes}`),
}));
// 在组件中使用
function BearCounter() {
const bears = useBearStore(state => state.bears); // 选择器
return <h1>{bears} bears</h1>;
}
// 也可在组件外使用
useBearStore.getState().increaseBears();中间件
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
const useStore = create(
devtools(
persist(
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
{ name: 'app-storage' } // localStorage key
),
{ name: 'AppStore' } // DevTools 中的 display name
)
);解决 Context 重渲染问题
Zustand 的选择器机制天然避免无效重渲染:
// ✅ 只有当 bears 变化时才会重渲染
const bears = useBearStore(s => s.bears);
// ✅ 返回原始值(bears 变时重渲染)
const bears = useBearStore(s => s.bears);
// ⚠️ 返回对象 → 每次都是新引用 → 总是重渲染
const { bears, fishes } = useBearStore(); // 不要这样用💡 最佳实践:使用选择器精确订阅最小状态片段。对象形式的选择器用
shallow比较器:useBearStore(s => ({b: s.bears, f: s.fishes}), shallow)。
异步 Action
const useUserStore = create((set, get) => ({
user: null,
loading: false,
fetchUser: async (id) => {
set({ loading: true });
try {
const user = await fetch(`/api/users/${id}`).then(r => r.json());
set({ user, loading: false });
} catch (error) {
set({ loading: false });
}
},
}));方案三:Redux Toolkit(RTK)
npm install @reduxjs/toolkit react-redux核心概念
| 概念 | 说明 |
|---|---|
| Slice | 状态切片 = reducer + actions 的集合 |
| Store | 单一状态树 |
| Selector | 从 store 中读取特定数据的函数 |
| Dispatch | 发送 action 触发状态变更 |
完整示例
// store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
incremented: state => { state.value += 1; },
decremented: state => { state.value -= 1; },
added: (state, action) => { state.value += action.payload; },
},
});
export const { incremented, decremented, added } = counterSlice.actions;
export default counterSlice.reducer;
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
// App.jsx
import { Provider } from 'react-redux';
import { store } from './store';
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
// Counter.jsx
import { useSelector, useDispatch } from 'react-redux';
import { incremented } from './store/counterSlice';
function Counter() {
const value = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return <button onClick={() => dispatch(incremented())}>
{value}
</button>;
}createAsyncThunk(处理异步)
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
export const fetchUsers = createAsyncThunk(
'users/fetchUsers',
async () => {
const response = await fetch('/api/users');
return response.json();
}
);
const usersSlice = createSlice({
name: 'users',
initialState: { items: [], status: 'idle' },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, state => { state.status = 'loading'; })
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchUsers.rejected, state => { state.status = 'failed'; });
},
});RTK Query(数据获取)
RTK 内置了 RTK Query,实现 API 缓存:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getUsers: builder.query({ query: () => '/users' }),
addUser: builder.mutation({ query: (user) => ({
url: '/users', method: 'POST', body: user
})}),
}),
});
export const { useGetUsersQuery, useAddUserMutation } = api;
// 在组件中
function UserList() {
const { data, error, isLoading } = useGetUsersQuery();
}React Query / TanStack Query(服务端状态)
服务端状态与客户端状态是两类不同的东西:
| 类型 | 示例 | 用谁管理 |
|---|---|---|
| 客户端状态 | 当前选中项、UI 开关、表单输入 | useState / Zustand / Redux |
| 服务端状态 | 用户列表、订单数据、API 响应 | React Query / SWR / RTK Query |
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function UserList() {
const { data, error, isLoading } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
});
}
function AddUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newUser) => fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] }); // 刷新列表
},
});
}💡 最佳实践:将服务端状态交给 React Query 管理,客户端状态交给 Zustand。大多数应用中,React Query 可以消除 70%+ 的全局状态代码。
选择决策树
状态只在当前组件内使用?
→ YES → useState
状态需要跨组件共享?
→ 父子间 → Props 下传
→ 兄弟/跨层级 →
→ 简单(1-3 个值) → Context
→ 复杂(多值、多更新) → Zustand
→ 团队已有 RTK 生态 → Redux Toolkit
数据来自服务端 API?
→ YES → React Query / SWR / RTK Query
需要持久化(localStorage)?
→ Zustand + persist 中间件