214 lines
6.1 KiB
Markdown
214 lines
6.1 KiB
Markdown
|
|
---
|
|||
|
|
tags: [React, State Management, Context, Zustand, Redux, Frontend]
|
|||
|
|
create time: 2026-04-29 22:08
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 状态管理
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
当组件树变得复杂,跨层级共享状态、服务器数据同步和状态变更追踪成为挑战。本文档对比 Context API、Zustand 和 Redux Toolkit 三种主流方案,帮助你在不同场景下做出正确选择。
|
|||
|
|
|
|||
|
|
## 选型决策图
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph TD
|
|||
|
|
A["需要全局状态吗?"] -->|"否"| B["useState / useReducer ✅"]
|
|||
|
|
A -->|"是"| C["状态类型?"]
|
|||
|
|
|
|||
|
|
C --> D["纯 UI 状态<br/>(主题、菜单展开、模态框)"]
|
|||
|
|
C --> E["服务器数据 / 异步缓存"]
|
|||
|
|
C --> F["应用级业务状态<br/>(用户信息、购物车、权限)"]
|
|||
|
|
|
|||
|
|
D --> G["Context API ✅"]
|
|||
|
|
E --> H["TanStack Query / SWR ✅"]
|
|||
|
|
F --> I["状态规模?"]
|
|||
|
|
|
|||
|
|
I --> J["小型 (< 5 store)"]
|
|||
|
|
I --> K["中大型"]
|
|||
|
|
|
|||
|
|
J --> L["Zustand ✅"]
|
|||
|
|
K --> M["Redux Toolkit + RTK Query ✅"]
|
|||
|
|
|
|||
|
|
style G fill:#4FC08D,color:#fff
|
|||
|
|
style H fill:#F5A87D,color:#000
|
|||
|
|
style L fill:#61DAFB,color:#000
|
|||
|
|
style M fill:#764abc,color:#fff
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Context API —— 轻量传递
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const AuthContext = createContext<{ user: User | null; login: (u: User) => void }>({
|
|||
|
|
user: null,
|
|||
|
|
login: () => {},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Provider
|
|||
|
|
function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||
|
|
const [user, setUser] = useState<User | null>(null);
|
|||
|
|
|
|||
|
|
const login = (u: User) => setUser(u);
|
|||
|
|
|
|||
|
|
return <AuthContext.Provider value={{ user, login }}>{children}</AuthContext.Provider>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Consumer
|
|||
|
|
function Profile() {
|
|||
|
|
const { user, login } = useContext(AuthContext);
|
|||
|
|
return <div>Hello, {user?.name}</div>;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Context 的性能局限
|
|||
|
|
|
|||
|
|
| 问题 | 原因 | 解决方案 |
|
|||
|
|
|------|------|----------|
|
|||
|
|
| value 变化时所有消费组件重渲染 | Context Value 引用每次都是新的 | 拆分多个 Context / 用 reducer 保持 dispatch 引用稳定 |
|
|||
|
|
| 不支持 selector | 没有 "只取子字段" 的机制 | 手动封装或使用第三方库 |
|
|||
|
|
| SSR hydration mismatch | 客户端与初始值不一致 | 延迟消费或用 useEffect 包裹 |
|
|||
|
|
|
|||
|
|
## Zustand —— 轻量级现代方案
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
import { create } from "zustand";
|
|||
|
|
|
|||
|
|
interface StoreState {
|
|||
|
|
count: number;
|
|||
|
|
users: User[];
|
|||
|
|
increment: () => void;
|
|||
|
|
fetchUsers: () => Promise<void>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const useStore = create<StoreState>((set, get) => ({
|
|||
|
|
count: 0,
|
|||
|
|
users: [],
|
|||
|
|
|
|||
|
|
increment: () => set(state => ({ count: state.count + 1 })),
|
|||
|
|
|
|||
|
|
fetchUsers: async () => {
|
|||
|
|
const res = await fetch("/api/users");
|
|||
|
|
const data = await res.json();
|
|||
|
|
set({ users: data });
|
|||
|
|
},
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
// 组件中使用
|
|||
|
|
function Counter() {
|
|||
|
|
// ✅ 只订阅 count —— 其他状态变化不会触发此组件重渲染
|
|||
|
|
const count = useStore(s => s.count);
|
|||
|
|
const increment = useStore(s => s.increment);
|
|||
|
|
|
|||
|
|
return <button onClick={increment}>Count: {count}</button>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 批量更新
|
|||
|
|
useStore.setState(({ count }) => ({ count: count + 1, flag: true }));
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Zustand 的优势
|
|||
|
|
|
|||
|
|
| 特性 | Zustand | Context |
|
|||
|
|
|------|---------|---------|
|
|||
|
|
| Bundle size | ~1KB | 内置 React(~0) |
|
|||
|
|
| Selector 支持 | ✅ 精确订阅 | ❌ 全部消费者都重渲染 |
|
|||
|
|
| TypeScript 推断 | 完善 | 需手动标注 |
|
|||
|
|
| DevTools | 原生支持 | 无 |
|
|||
|
|
| 中间件扩展 | persist、immer、devtools | 需额外封装 |
|
|||
|
|
|
|||
|
|
## Redux Toolkit —— 企业级方案
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
import { createSlice, configureStore, useDispatch, useSelector } from "@reduxjs/toolkit";
|
|||
|
|
|
|||
|
|
interface CounterSlice {
|
|||
|
|
value: number;
|
|||
|
|
status: "idle" | "loading" | "succeeded" | "failed";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const counterSlice = createSlice({
|
|||
|
|
name: "counter",
|
|||
|
|
initialState: { value: 0, status: "idle" } as CounterSlice,
|
|||
|
|
reducers: {
|
|||
|
|
incremented: state => { state.value += 1; }, // ✅ Immer:直接 mutate!
|
|||
|
|
fetchedAsync: {
|
|||
|
|
pending: state => { state.status = "loading"; },
|
|||
|
|
fulfilled: (state, action) => {
|
|||
|
|
state.status = "succeeded";
|
|||
|
|
state.value = action.payload.value;
|
|||
|
|
},
|
|||
|
|
rejected: state => { state.status = "failed"; },
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
export const { incremented, fetchedAsync } = counterSlice.actions;
|
|||
|
|
|
|||
|
|
const store = configureStore({
|
|||
|
|
reducer: { counter: counterSlice.reducer },
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
function CounterComponent() {
|
|||
|
|
const dispatch = useDispatch<AppDispatch>();
|
|||
|
|
const count = useSelector((s: AppState) => s.counter.value);
|
|||
|
|
|
|||
|
|
return <button onClick={() => dispatch(incremented())}>{count}</button>;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### RTK Query —— 内置数据获取
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
|||
|
|
|
|||
|
|
const api = createApi({
|
|||
|
|
reducerPath: "api",
|
|||
|
|
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
|
|||
|
|
endpoints: build => ({
|
|||
|
|
getUsers: build.query<User[], void>({ query: () => "/users" }),
|
|||
|
|
updateUser: build.mutation<User, Partial<User>>({
|
|||
|
|
query: ({ id, ...patch }) => ({ url: `/users/${id}`, method: "PATCH", body: patch }),
|
|||
|
|
}),
|
|||
|
|
}),
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
export const { useGetUsersQuery, useUpdateUserMutation } = api;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 三框架横向对比
|
|||
|
|
|
|||
|
|
| 维度 | Context API | Zustand | Redux Toolkit |
|
|||
|
|
|------|-------------|---------|---------------|
|
|||
|
|
| Bundle 大小 | ~0KB(内置) | ~1KB | ~15KB |
|
|||
|
|
| 学习曲线 | 低 | 低 | 中 |
|
|||
|
|
| Selector | ❌ | ✅ | ✅(useSelector) |
|
|||
|
|
| Immutable | ❌ | ❌(但可用 immer middleware) | ✅(Immer 内置) |
|
|||
|
|
| 调试工具 | ❌ | ✅ | ✅ Redux DevTools |
|
|||
|
|
| 服务端渲染 | ⚠️ 需手动处理 | ✅ | ✅ |
|
|||
|
|
| 适用规模 | 小型项目 | 中小/中型 | 大型企业级 |
|
|||
|
|
|
|||
|
|
## 常见反模式
|
|||
|
|
|
|||
|
|
> [!warning] 以下做法应避免
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// ❌ 把所有东西塞进一个 store
|
|||
|
|
const useBigStore = create(() => ({
|
|||
|
|
user: ..., theme: ..., sidebarOpen: ..., notifications: ..., cart: ..., preferences: ..., // 20+ 个状态
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
// ✅ 按功能拆分为多个 store
|
|||
|
|
const useUserStore = create(...)
|
|||
|
|
const useThemeStore = create(...)
|
|||
|
|
|
|||
|
|
// ❌ 在 state 中存储服务器返回的数据却不做缓存
|
|||
|
|
const [data, setData] = useState(fetch(...)) // 页面切换丢失,重复请求
|
|||
|
|
|
|||
|
|
// ✅ 使用 TanStack Query 等专用数据获取库处理缓存和失效
|
|||
|
|
const { data } = useQuery({ queryKey: ["users"], queryFn: fetchUsers });
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 关联笔记
|