--- 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
(theme, menu, modal)"] C --> E["Server data / async cache"] C --> F["App-level business state
(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(null); const login = (u: User) => setUser(u); return {children}; } // Consumer function Profile() { const { user, login } = useContext(AuthContext); return
Hello, {user?.name}
; } ``` ### 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 {children}; } // ✅ 正确做法 — useMemo + useCallback 双重稳定化 function GoodProvider({ children }: { children: React.ReactNode }) { const [user, login] = useAuth(); const value = useMemo(() => ({ user, login }), [user, login]); return {children}; } ``` ## 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; } const useStore = create((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 ; } // ⚠️ 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()( // ✅ 多个中间件可以叠加使用 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(); // ✅ useSelector 自带 selector(浅比较)—— 只有 value 变化时才重渲染 const count = useSelector((s: AppState) => s.counter.value); return ; } ``` ### 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({ query: () => "/users" }), updateUser: build.mutation>({ 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 设计中的最佳实践