React State Management 技能实战指南:Redux Toolkit、Zustand、Jotai 与 React Query 的选型与落地
本篇技术指南以 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
解读如下:
- 小型应用、简单状态:
Zustand或Jotai。两者 API 极简、样板代码几乎为零,无需 Provider 包裹(Zustand 甚至支持在组件外读写),非常适合快速迭代。 - 大型应用、复杂状态:
Redux Toolkit。当状态图庞大、更新路径多、需要严格可预测性与 DevTools 时间旅行调试时,RTK 的 slice + createAsyncThunk + Immer 方案能提供工程化约束。 - 强服务端交互:用
React Query(或 SWR)承载服务端数据,再搭配一个轻量客户端状态库(如 Zustand)处理 UI 瞬时状态。这是技能反复强调的**关注点分离(separate concerns)**范式。 - 原子化/细粒度更新:
Jotai。以原子(atom)为最小单位,天然适合需要精确订阅、避免整棵组件树重渲染的场景。
快速上手:Zustand 最小可运行示例
技能文档以 Zustand 作为"最简单"的入口,给出了带 devtools 与 persist 中间件的完整示例,这里逐段拆解。
// 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' }
)
)
)
要点说明:
create<AppState>()(...):Zustand 泛型工厂函数,interface AppState同时约束 Store 的数据与 action 签名,保证类型安全。devtools(...):接入 Redux DevTools 中间件,开发期可查看每个 action、进行时间旅行调试。persist(..., { name: 'app-storage' }):将 Store 状态持久化到localStorage,key 为app-storage,刷新页面后用户与主题状态自动恢复。- 函数式更新:
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;
值得注意的工程细节:
- 类型推导:
RootState与AppDispatch直接从 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"原则的体现——不要把 isLoading、hasError 等派生布尔值各自散落存储。
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 的层级设计(all → lists → list(filters) → details → detail(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>
)
}
sidebarOpen、modal 这类纯 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:派生值(如
isAuthenticated、isLoading)应通过 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 语句全部收敛为一个
createSlice。createSlice自动生成同名的 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 install或npx skills add直接安装单个技能到任意 Agent(见 README 与 docs/agent-skills.md)。
技能采用"导航层 + 详情层"结构:SKILL.md 提供分类、选型与快速上手,references/details.md 提供五种完整模式,按需渐进加载,与仓库整体的 token 效率设计一致。
总结
React 状态管理没有银弹,但有清晰的决策路径:先按"局部 / 全局 / 服务端 / URL / 表单"五类划分状态,再按应用规模选择 Redux Toolkit(大型复杂)、Zustand / Jotai(轻量敏捷)、React Query(服务端缓存与乐观更新),并严格遵循"状态就近存放、选择器细粒度订阅、派生值现算、客户端与服务端状态分离"的最佳实践。将本技能文档与 references/details.md 中的五种模式配合使用,即可在一线 React 项目中直接落地这套完整的状态管理架构。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.2 K634
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown300
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java101
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java60
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript60
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python280