Files
cs-note/hhs/REACT/3. 生态工具篇/09-状态管理.md
T
2026-05-24 11:42:38 +08:00

322 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [React, State Management, Context, Zustand, Redux, Frontend]
create time: 2026-04-29 22:08
---
# 状态管理
## 概述
当组件树变得复杂,跨层级共享状态、服务器数据同步和状态变更追踪成为挑战。本文档对比 Context API、Zustand 和 Redux Toolkit 三种主流方案,帮助你在不同场景下做出正确选择。
## 选型决策图
```mermaid
graph TD
A["Do you need global state?"] -->|"No"| B["useState / useReducer ✅"]
A -->|"Yes"| C["State type?"]
C --> D["Pure UI state<br/>(theme, menu, modal)"]
C --> E["Server data / async cache"]
C --> F["App-level business state<br/>(user info, cart, permissions)"]
D --> G["Context API ✅"]
E --> H["TanStack Query / SWR ✅"]
F --> I["State size?"]
I --> J["Small (< 5 stores)"]
I --> K["Medium-Large"]
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 的性能局限
> [!question] 为什么 value 变化会导致所有消费组件重渲染?
> 因为 Context.Provider.value 是一个引用类型。每次 `value={{ user, login }}` 都会创建一个新对象,React 比较的是引用地址而非内容——地址不同就判定为"值变了"。
| 问题 | 原因 | 解决方案 |
|------|------|----------|
| value 变化时所有消费组件重渲染 | Context Value 引用每次都是新的 | 拆分多个 Context / 用 reducer 保持 dispatch 引用稳定 |
| 不支持 selector | 没有"只取子字段"的机制 | 手动封装或使用第三方库 |
| SSR hydration mismatch | 客户端与初始值不一致 | 延迟消费或用 useEffect 包裹 |
### Context 最佳实践 checklist
> [!tip] 使用 Context 时的关键要点
> - ✅ 始终用 `useMemo` 包装 Provider value(防止每次渲染创建新对象)
> - ✅ 函数型 props 用 `useCallback` 缓存(减少不必要的消费者重渲染)
> - ✅ 将"低频更新"与"高频更新"拆分为独立 Context
> - ❌ 避免在 Context 中存储大量频繁变化的数据(如输入框实时值)
> - ❌ 不要把 Context 当作全局状态管理的全能替代方案
```tsx
// ❌ 反模式 — value 每次都是新引用,所有 Consumer 会无条件重渲染
function BadProvider() {
const [user, login] = useAuth();
return <AuthContext.Provider value={{ user, login }}>{children}</AuthContext.Provider>;
}
// ✅ 正确做法 — useMemo + useCallback 双重稳定化
function GoodProvider({ children }: { children: React.ReactNode }) {
const [user, login] = useAuth();
const value = useMemo(() => ({ user, login }), [user, login]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
```
## Zustand —— 轻量级现代方案
> [!tip] Zustand 的核心理念:像用 state 一样用 store
> 不需要 Provider、不需要 dispatch、不需要 reducer。直接 create → 直接 use,零样板代码。
```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: [],
// ✅ set 接受函数形式可以拿到上一次状态 — 避免闭包陷阱
increment: () => set(state => ({ count: state.count + 1 })),
// ✅ 异步操作直接在 action 里写
fetchUsers: async () => {
const res = await fetch("/api/users");
const data = await res.json();
set({ users: data });
},
}));
// 组件中使用
function Counter() {
// ✅ selector 模式:只订阅 count,users/increment 变化不会触发此组件重渲染
const count = useStore(s => s.count);
const increment = useStore(s => s.increment);
return <button onClick={increment}>Count: {count}</button>;
}
// ⚠️ setState 直接修改(不通过 hook),适用于回调和非 React 环境
useStore.setState(({ count }) => ({ count: count + 1 }));
```
### Zustand 中间件
> [!example] persist 中间件:自动将状态同步到 localStorage
> 刷新页面后数据不丢失,非常适合持久化用户偏好设置。
```ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface ThemeState {
mode: "light" | "dark";
toggle: () => void;
}
const useThemeStore = create<ThemeState>()(
// ✅ 多个中间件可以叠加使用
persist(
(set) => ({
mode: "light",
toggle: () => set(state => ({ mode: state.mode === "light" ? "dark" : "light" })),
}),
{ name: "theme-storage" }, // localStorage key
),
);
// ⚠️ persist 默认只序列化为 JSON,不支持 Date / RegExp / Function 等复杂类型
```
### Zustand 的优势
| 特性 | Zustand | Context |
|------|---------|---------|
| Bundle size | ~1KB | 内置 React(~0) |
| Selector 支持 | ✅ 精确订阅 | ❌ 全部消费者都重渲染 |
| TypeScript 推断 | 完善 | 需手动标注 |
| DevTools | 原生支持 | 无 |
| 中间件扩展 | persist、immer、devtools | 需额外封装 |
## Redux Toolkit —— 企业级方案
> [!question] Redux Toolkit vs 旧版 Redux:为什么要用 RTK?
> 在 Redux Toolkit 出现之前,Redux 需要写 action types、action creators、switch-case reducer——样板代码极多。RTK 通过 `createSlice` + Immer,将模板代码减少 80% 以上,同时保留 Redux 的调试能力和可预测性。
```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: {
// ✅ extraReducers builder pattern — 类型安全的事件监听
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 },
});
// configureStore 自动集成了 devtools、redux-thunk、reducer 组合 — 不需要手写
```
```tsx
function CounterComponent() {
const dispatch = useDispatch<AppDispatch>();
// ✅ useSelector 自带 selector(浅比较)—— 只有 value 变化时才重渲染
const count = useSelector((s: AppState) => s.counter.value);
return <button onClick={() => dispatch(incremented())}>{count}</button>;
}
```
### RTK Query —— 内置数据获取
> [!tip] RTK Query 的定位:替代 Axios + useEffect + useState 的组合拳
> 自动处理缓存、loading 状态、重试、增量更新——把数据获取变成声明式。
```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;
// ⚠️ 导出的是 hook — 组件中直接解构使用,无需手动 dispatch
```
## 状态管理问题排查流程
> [!question] 💡 你的应用遇到了什么问题?对照以下流程图定位根因
```mermaid
flowchart TD
A["Performance Issue?"] --> B["Unnecessary Re-render?"]
A --> C["State Lost / Out-of-sync?"]
B --> D["Reading full Context value?"]
D -->|"Yes"| E["Switch to selector / Split Context ✅"]
D -->|"No"| F["useSelector missing selector arg?"]
F -->|"Missing"| G["Change to useSelector s => s.xxx ✅"]
F -->|"OK"| H["Check React StrictMode double-execution"]
C --> I["Using Context for server data?"]
I -->|"Yes"| J["Switch to TanStack Query / RTK Query ✅"]
I -->|"No"| K["Zustand store missing persist?"]
K -->|"Yes"| L["Add persist middleware ✅"]
K -->|"No"| M["Store destroyed on route change?"]
H --> N["Expected behavior — confirm if it causes real bugs"]
J --> O["Cache hit — no repeated requests"]
L --> P["Restores state after refresh"]
M --> Q["Check app mount lifecycle"]
```
## 三框架横向对比
| 维度 | Context API | Zustand | Redux Toolkit |
|------|-------------|---------|---------------|
| Bundle 大小 | ~0KB(内置) | ~1KB | ~15KB |
| 学习曲线 | 低 | 低 | 中 |
| Selector | ❌ | ✅ | ✅(useSelector) |
| Immutable | ❌ | ❌(但可用 immer middleware) | ✅(Immer 内置) |
| 调试工具 | ❌ | ✅ | ✅ Redux DevTools |
| 服务端渲染 | ⚠️ 需手动处理 | ✅ | ✅ |
| 适用规模 | 小型项目 | 中小/中型 | 大型企业级 |
## 常见反模式
> [!warning] 以下做法应避免
```tsx
// ❌ 把所有东西塞进一个 store — 导致 selector 粒度太粗、难以维护
const useBigStore = create(() => ({
user: ..., theme: ..., sidebarOpen: ..., notifications: ..., cart: ..., preferences: ..., // 20+ 个状态
}));
// ✅ 按功能拆分为多个 store — 职责单一,selector 精确
const useUserStore = create(...)
const useThemeStore = create(...)
```
```tsx
// ❌ 在 state 中存储服务器返回的数据却不做缓存
const [data, setData] = useState(fetch(...)) // 页面切换丢失,重复请求
// 问题:每次切到同一页面都会重新请求,且没有 loading / error 状态管理
// ✅ 使用 TanStack Query 等专用数据获取库处理缓存和失效
const { data, isLoading, error } = useQuery({ queryKey: ["users"], queryFn: fetchUsers });
```
> [!tip] 📋 状态管理最佳实践总结
> 1. **职责分离原则** — UI 状态用 useState,全局状态用 Store,服务器数据用 Query
> 2. **选择最小可行方案** — 能用 Context 解决的问题,不要引入 Zustand;能用 Zustand 的,不要上 Redux
> 3. **按需订阅** — 无论哪个库,尽量用 selector 精确订阅,避免大面积重渲染
> 4. **类型先行** — TypeScript 项目中,先定义 State interface 再写 Store,让编译器帮你守住边界
## 关联笔记
- [[3. 生态工具篇/08-路由管理]] — 路由与状态的关系:URL 也是状态的一种表现形式
- [[3. 生态工具篇/10-TS + React]] — TypeScript 类型定义在 Store 设计中的最佳实践