首页
/ React State Management 技能实战指南:Redux Toolkit、Zustand、Jotai 与 React Query 的选型与落地

React State Management 技能实战指南:Redux Toolkit、Zustand、Jotai 与 React Query 的选型与落地

2026-09-09 16:51:42作者:郜逊炳

本篇技术指南以 agents24/agents 仓库中 react-state-management 技能 为主体,系统讲解现代 React 状态管理的完整图谱:从局部组件状态到全局 Store、再到服务端状态同步。读者学完后,将掌握五类状态的划分方法、四大主流方案(Redux Toolkit / Zustand / Jotai / React Query)的选型依据、可复制运行的 TypeScript 实现模板,以及从遗留 Redux 平滑迁移到 RTK 的实操路径。

技能定位:渐进式披露的知识包

在 agents24/agents 仓库中,frontend-mobile-development 插件 下的 skills/ 目录存放模块化知识包(modular knowledge packages),采用**渐进式披露(progressive disclosure)**机制:SKILL.md 作为导航层,只保留高频决策信息;更深的模式文档放在 references/details.md 中,仅在导航层不足以应对时读取,以控制 token 消耗。

该技能的 frontmatter 明确了触发场景:

name: react-state-management
description: Master modern React state management with Redux Toolkit, Zustand, Jotai, and React Query. Use when setting up global state, managing server state, or choosing between state management solutions.

即:在设置全局状态、管理服务端状态、或在多个状态管理方案间做技术选型时,应主动启用此技能。它与同插件的 frontend-developer agent 形成互补——该 Agent 的职责描述明确包含 "Modern state management with Zustand, Jotai, and Valtio"、"React Query/TanStack Query for server state management"、"Redux Toolkit for complex state scenarios",技能为其提供具体落地方案。

核心概念:五类状态的划分

现代 React 应用的"状态"并非铁板一块,将状态按来源与生命周期分类,是正确选型的第一步。技能文档给出了权威分类表:

Type Description Solutions
Local State Component-specific, UI state useState, useReducer
Global State Shared across components Redux Toolkit, Zustand, Jotai
Server State Remote data, caching React Query, SWR, RTK Query
URL State Route parameters, search React Router, nuqs
Form State Input values, validation React Hook Form, Formik

五个类别对应五类工具生态,各有明确边界:

  • 局部状态:只服务于单个组件(下拉菜单展开、Tab 切换、输入框受控值),优先 useState;状态更新逻辑复杂时(多 action 分支、相互依赖的状态)升级为 useReducer
  • 全局状态:多个组件共享、需要跨树访问(用户会话、主题、购物车),交给 Redux Toolkit / Zustand / Jotai 这类带订阅机制的外部 Store。
  • 服务端状态:来自 API 的远程数据。它的特殊性在于同时存在缓存、失效、重试、竞态等"非前端"问题,应交给专门的数据请求库,而非塞进全局 Store。
  • URL 状态:筛选条件、分页、搜索词等应反映在 URL 中,便于分享与回退,由路由库承担。
  • 表单状态:字段值、校验、脏标记、提交状态,用 React Hook Form / Formik 承担。

这一分类直接呼应技能"Best Practices"中的两条核心原则:Colocate state(状态尽量贴近使用处)与 Don't over-globalize(并非所有状态都该进全局 Store)。

选型标准:按应用规模匹配方案

技能文档给出了一段精炼的决策脚本,按应用规模与形态选择主方案:

Small app, simple state → Zustand or Jotai
Large app, complex state → Redux Toolkit
Heavy server interaction → React Query + light client state
Atomic/granular updates → Jotai

解读如下:

  • 小型应用、简单状态ZustandJotai。两者 API 极简、样板代码几乎为零,无需 Provider 包裹(Zustand 甚至支持在组件外读写),非常适合快速迭代。
  • 大型应用、复杂状态Redux Toolkit。当状态图庞大、更新路径多、需要严格可预测性与 DevTools 时间旅行调试时,RTK 的 slice + createAsyncThunk + Immer 方案能提供工程化约束。
  • 强服务端交互:用 React Query(或 SWR)承载服务端数据,再搭配一个轻量客户端状态库(如 Zustand)处理 UI 瞬时状态。这是技能反复强调的**关注点分离(separate concerns)**范式。
  • 原子化/细粒度更新Jotai。以原子(atom)为最小单位,天然适合需要精确订阅、避免整棵组件树重渲染的场景。

快速上手:Zustand 最小可运行示例

技能文档以 Zustand 作为"最简单"的入口,给出了带 devtoolspersist 中间件的完整示例,这里逐段拆解。

// store/useStore.ts
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'

interface AppState {
  user: User | null
  theme: 'light' | 'dark'
  setUser: (user: User | null) => void
  toggleTheme: () => void
}

export const useStore = create<AppState>()(
  devtools(
    persist(
      (set) => ({
        user: null,
        theme: 'light',
        setUser: (user) => set({ user }),
        toggleTheme: () => set((state) => ({
          theme: state.theme === 'light' ? 'dark' : 'light'
        })),
      }),
      { name: 'app-storage' }
    )
  )
)

要点说明:

  1. create<AppState>()(...):Zustand 泛型工厂函数,interface AppState 同时约束 Store 的数据与 action 签名,保证类型安全。
  2. devtools(...):接入 Redux DevTools 中间件,开发期可查看每个 action、进行时间旅行调试。
  3. persist(..., { name: 'app-storage' }):将 Store 状态持久化到 localStorage,key 为 app-storage,刷新页面后用户与主题状态自动恢复。
  4. 函数式更新toggleTheme 使用 set((state) => ...) 基于当前状态计算新值,避免闭包捕获过期状态。

组件内消费极其直接,无需 Provider:

// Usage in component
function Header() {
  const { user, theme, toggleTheme } = useStore()
  return (
    <header className={theme}>
      {user?.name}
      <button onClick={toggleTheme}>Toggle Theme</button>
    </header>
  )
}

useStore() 直接解构即完成订阅,状态变化自动触发重渲染。

五种实战模式详解

详细模式文档位于 references/details.md,覆盖从全局 Store 到服务端缓存的完整工程形态。

Pattern 1:Redux Toolkit + TypeScript 标准化配置

适合大型应用建立全局 Store 基础设施。核心是 configureStore 组合多个 reducer,并导出类型安全的 hooks:

// store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import userReducer from "./slices/userSlice";
import cartReducer from "./slices/cartSlice";

export const store = configureStore({
  reducer: {
    user: userReducer,
    cart: cartReducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: ["persist/PERSIST"],
      },
    }),
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

// Typed hooks
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

值得注意的工程细节:

  • 类型推导RootStateAppDispatch 直接从 Store 实例推导,新增 slice 后无需手改类型,避免类型漂移。
  • serializableCheck:RTK 默认禁止在 action 中携带不可序列化值(如 Date、Promise)。持久化中间件(如 redux-persist)的初始化 action 是已知例外,通过 ignoredActions: ["persist/PERSIST"] 显式放行,这是实践中常见的"坑"与对应解法。

异步逻辑用 createAsyncThunk 编写,slice 内通过 extraReducers 响应其生命周期:

// store/slices/userSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";

interface UserState {
  current: User | null;
  status: "idle" | "loading" | "succeeded" | "failed";
  error: string | null;
}

const initialState: UserState = { current: null, status: "idle", error: null };

export const fetchUser = createAsyncThunk(
  "user/fetchUser",
  async (userId: string, { rejectWithValue }) => {
    try {
      const response = await fetch(`/api/users/${userId}`);
      if (!response.ok) throw new Error("Failed to fetch user");
      return await response.json();
    } catch (error) {
      return rejectWithValue((error as Error).message);
    }
  },
);

const userSlice = createSlice({
  name: "user",
  initialState,
  reducers: {
    setUser: (state, action: PayloadAction<User>) => {
      state.current = action.payload;
      state.status = "succeeded";
    },
    clearUser: (state) => {
      state.current = null;
      state.status = "idle";
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => {
        state.status = "loading";
        state.error = null;
      })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.status = "succeeded";
        state.current = action.payload;
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.payload as string;
      });
  },
});

export const { setUser, clearUser } = userSlice.actions;
export default userSlice.reducer;

status 字段采用 "idle" | "loading" | "succeeded" | "failed" 显式状态机,UI 层可据此渲染加载/错误/成功三种视图,是"Don't store derived data"原则的体现——不要把 isLoadinghasError 等派生布尔值各自散落存储。

Pattern 2:Zustand 的 Slices 模式(可扩展)

Zustand 原生 API 足够简单,但当 Store 字段与 action 增多时,单文件会迅速膨胀。Slices 模式按领域切分 Store,再用 StateCreator 保证类型安全并合并:

// store/slices/createUserSlice.ts
import { StateCreator } from "zustand";

export interface UserSlice {
  user: User | null;
  isAuthenticated: boolean;
  login: (credentials: Credentials) => Promise<void>;
  logout: () => void;
}

export const createUserSlice: StateCreator<
  UserSlice & CartSlice, // Combined store type
  [],
  [],
  UserSlice
> = (set, get) => ({
  user: null,
  isAuthenticated: false,
  login: async (credentials) => {
    const user = await authApi.login(credentials);
    set({ user, isAuthenticated: true });
  },
  logout: () => {
    set({ user: null, isAuthenticated: false });
    // Can access other slices
    // get().clearCart()
  },
});

注意 StateCreator<UserSlice & CartSlice, [], [], UserSlice> 的第一个泛型参数是合并后的完整 Store 类型,这使单个 slice 内部可以通过 get() 访问其他 slice 的状态与 action(如登出时调用 get().clearCart())。

合并多个 slice 只需展开组合:

// store/index.ts
import { create } from "zustand";
import { createUserSlice, UserSlice } from "./slices/createUserSlice";
import { createCartSlice, CartSlice } from "./slices/createCartSlice";

type StoreState = UserSlice & CartSlice;

export const useStore = create<StoreState>()((...args) => ({
  ...createUserSlice(...args),
  ...createCartSlice(...args),
}));

// Selective subscriptions (prevents unnecessary re-renders)
export const useUser = () => useStore((state) => state.user);
export const useCart = () => useStore((state) => state.cart);

useUser / useCart 通过选择器订阅(selective subscriptions)只监听所需字段,配合 useStore((state) => state.user) 这种按字段粒度取值的写法,可有效避免组件因无关字段变化而重渲染——这正是技能 Best Practices 中 Use selectors 的落地。

Pattern 3:Jotai 原子化状态

Jotai 以"原子"为基本单元,支持派生、持久化、异步与只写 action,粒度和组合能力都极强:

// atoms/userAtoms.ts
import { atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'

// Basic atom
export const userAtom = atom<User | null>(null)

// Derived atom (computed)
export const isAuthenticatedAtom = atom((get) => get(userAtom) !== null)

// Atom with localStorage persistence
export const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light')

// Async atom
export const userProfileAtom = atom(async (get) => {
  const user = get(userAtom)
  if (!user) return null
  const response = await fetch(`/api/users/${user.id}/profile`)
  return response.json()
})

// Write-only atom (action)
export const logoutAtom = atom(null, (get, set) => {
  set(userAtom, null)
  set(cartAtom, [])
  localStorage.removeItem('token')
})

五种原子形态各司其职:

  • 基础原子atom<User | null>(null) 保存原始状态;
  • 派生原子isAuthenticatedAtom 通过 get(userAtom) 计算派生值,不重复存储(对应 Best Practices 的 "Don't store derived data"),依赖的 userAtom 变化时自动重算;
  • 持久化原子atomWithStorage 一行完成 localStorage 读写;
  • 异步原子:async atom 可直接返回 Promise,组件内通过 Suspense 消费;
  • 只写原子(action):第一参数传 null,专门用于组合多个原子写入操作。

组件内配合 Suspense 消费异步原子:

function Profile() {
  const [user] = useAtom(userAtom)
  const [, logout] = useAtom(logoutAtom)
  const [profile] = useAtom(userProfileAtom) // Suspense-enabled

  return (
    <Suspense fallback={<Skeleton />}>
      <ProfileContent profile={profile} onLogout={logout} />
    </Suspense>
  )
}

Pattern 4:React Query 管理服务端状态

服务端状态的核心矛盾在于"缓存与一致性"。React Query 提供 query key 体系、staleTime / gcTime 缓存策略与乐观更新机制。模式文档先给出 query keys 工厂函数:

// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

// Query keys factory
export const userKeys = {
  all: ["users"] as const,
  lists: () => [...userKeys.all, "list"] as const,
  list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
  details: () => [...userKeys.all, "detail"] as const,
  detail: (id: string) => [...userKeys.details(), id] as const,
};

Query key 的层级设计(alllistslist(filters)detailsdetail(id))使缓存失效可以按粒度精确操作:失效整个集合、整个列表、或单个用户。

export function useUsers(filters: UserFilters) {
  return useQuery({
    queryKey: userKeys.list(filters),
    queryFn: () => fetchUsers(filters),
    staleTime: 5 * 60 * 1000, // 5 minutes
    gcTime: 30 * 60 * 1000, // 30 minutes (formerly cacheTime)
  });
}

export function useUser(id: string) {
  return useQuery({
    queryKey: userKeys.detail(id),
    queryFn: () => fetchUser(id),
    enabled: !!id, // Don't fetch if no id
  });
}
  • staleTime(5 分钟):数据在 5 分钟内被视为"新鲜",重复挂载组件不会触发重新请求;
  • gcTime(30 分钟,旧称 cacheTime:卸载后缓存保留 30 分钟,期间重新挂载可秒开;
  • enabled: !!id:无 id 时不发请求,避免无效网络往返。

**乐观更新(optimistic update)**是 React Query 提升交互体验的关键模式——先更新 UI,失败再回滚:

export function useUpdateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: updateUser,
    onMutate: async (newUser) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({
        queryKey: userKeys.detail(newUser.id),
      });

      // Snapshot previous value
      const previousUser = queryClient.getQueryData(
        userKeys.detail(newUser.id),
      );

      // Optimistically update
      queryClient.setQueryData(userKeys.detail(newUser.id), newUser);

      return { previousUser };
    },
    onError: (err, newUser, context) => {
      // Rollback on error
      queryClient.setQueryData(
        userKeys.detail(newUser.id),
        context?.previousUser,
      );
    },
    onSettled: (data, error, variables) => {
      // Refetch after mutation
      queryClient.invalidateQueries({
        queryKey: userKeys.detail(variables.id),
      });
    },
  });
}

完整的三段式流程:onMutate 取消进行中的请求 → 快照旧值 → 立即写入新值;onError 用快照回滚;onSettled 无论成败都使缓存失效以拉取服务端最终结果。快照通过 mutation 的 context 参数在三个阶段间传递。

Pattern 5:客户端 + 服务端状态组合

生产级应用几乎必然是混合架构:Zustand 管客户端瞬时状态,React Query 管服务端数据。技能文档给出了典型的 Dashboard 组合:

// Zustand for client state
const useUIStore = create<UIState>((set) => ({
  sidebarOpen: true,
  modal: null,
  toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
  openModal: (modal) => set({ modal }),
  closeModal: () => set({ modal: null }),
}))

// React Query for server state
function Dashboard() {
  const { sidebarOpen, toggleSidebar } = useUIStore()
  const { data: users, isLoading } = useUsers({ active: true })
  const { data: stats } = useStats()

  if (isLoading) return <DashboardSkeleton />

  return (
    <div className={sidebarOpen ? 'with-sidebar' : ''}>
      <Sidebar open={sidebarOpen} onToggle={toggleSidebar} />
      <main>
        <StatsCards stats={stats} />
        <UserTable users={users} />
      </main>
    </div>
  )
}

sidebarOpenmodal 这类纯 UI 瞬时状态与用户/统计这类远程数据各归其位:Zustand 负责界面响应,React Query 负责缓存与数据新鲜度,两者互不污染。这正是技能 Best Practices 中 Separate concerns(服务端状态交给 React Query,客户端状态交给 Zustand)与 Don't duplicate server state(不要让全局 Store 保存 API 数据的副本)的直接体现。

最佳实践清单

技能文档给出了简洁的行动准则,这里逐条补充原理:

Do's(应当)

  • Colocate state:状态放在离使用处最近的层级,直到"确实需要共享"再提升,避免过早抽象。
  • Use selectors:用选择器做细粒度订阅(如 useStore((state) => state.user)),防止无关字段变化触发整组件重渲染。
  • Normalize data:将嵌套结构扁平化(如 { byId: {...}, allIds: [...] }),使更新单条记录不必深拷贝整棵对象树。
  • Type everything:全量 TypeScript 覆盖(Store 接口、slice 类型、query key)把运行时错误提前到编译期。仓库内的 component-scaffold 命令 同样以 TS 接口驱动组件生成,与技能理念一致。
  • Separate concerns:服务端状态(React Query)与客户端状态(Zustand)分而治之。

Don'ts(禁忌)

  • Don't over-globalize:不是所有状态都需要全局化,能放在组件局部就不要提升。
  • Don't duplicate server state:服务端数据交给 React Query 管理,全局 Store 只留一份权威副本,避免双写不同步。
  • Don't mutate directly:始终不可变更新(Zustand/Redux 的 set 语义天然如此;RTK 内依赖 Immer 允许"看起来像 mutation"的写法,但那是库代劳的不可变更新)。
  • Don't store derived data:派生值(如 isAuthenticatedisLoading)应通过 selector / derived atom / 计算属性现算,而非另存一份副本,防止源数据与派生值失步。
  • Don't mix paradigms:同一类别内只选一个主方案(如全局状态统一用 Zustand,而不是一部分 Redux、一部分 Jotai),降低认知负担与协作成本。

迁移指南:从遗留 Redux 到 Redux Toolkit

技能文档最后给出最常遇到的迁移场景——把传统手写 action + switch reducer 重构为 RTK slice。两者对比如下:

// Before (legacy Redux)
const ADD_TODO = "ADD_TODO";
const addTodo = (text) => ({ type: ADD_TODO, payload: text });
function todosReducer(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [...state, { text: action.payload, completed: false }];
    default:
      return state;
  }
}

// After (Redux Toolkit)
const todosSlice = createSlice({
  name: "todos",
  initialState: [],
  reducers: {
    addTodo: (state, action: PayloadAction<string>) => {
      // Immer allows "mutations"
      state.push({ text: action.payload, completed: false });
    },
  },
});

迁移要点:

  • 样板代码大幅缩减:常量字符串 action type、action creator 工厂、switch 语句全部收敛为一个 createSlicecreateSlice 自动生成同名的 action creators 与 reducer,且 state.push(...) 这种"看起来可变"的写法由 Immer 在底层转成不可变更新,杜绝手写展开运算符出错的可能。
  • 类型安全增强PayloadAction<string> 明确约束 action 载荷类型,编译器即可拦截类型错误。
  • 配套升级:结合 Pattern 1,用 configureStore 取代 createStore + 手动中间件组合,并导出类型化 hooks(useAppDispatch / useAppSelector)贯穿应用。

在仓库生态中的使用方式

该技能是 frontend-mobile-development 插件的组成部分。仓库支持多种安装途径:

  • Claude Code:添加 marketplace 后 install frontend-mobile-development 插件,技能随插件加载(见 docs/plugins.md);
  • Skills-only 安装:通过 gh skill installnpx skills add 直接安装单个技能到任意 Agent(见 READMEdocs/agent-skills.md)。

技能采用"导航层 + 详情层"结构:SKILL.md 提供分类、选型与快速上手,references/details.md 提供五种完整模式,按需渐进加载,与仓库整体的 token 效率设计一致。

总结

React 状态管理没有银弹,但有清晰的决策路径:先按"局部 / 全局 / 服务端 / URL / 表单"五类划分状态,再按应用规模选择 Redux Toolkit(大型复杂)、Zustand / Jotai(轻量敏捷)、React Query(服务端缓存与乐观更新),并严格遵循"状态就近存放、选择器细粒度订阅、派生值现算、客户端与服务端状态分离"的最佳实践。将本技能文档与 references/details.md 中的五种模式配合使用,即可在一线 React 项目中直接落地这套完整的状态管理架构。

热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
900
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
927
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.94 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
603
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
396
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
527