🚀 快速安装
复制以下命令并运行,立即安装此 Skill:
npx skills add https://github.com/wshobson/agents --skill react-state-management
💡 提示:需要 Node.js 和 NPM
React 状态管理
关于现代 React 状态管理模式的全面指南,涵盖从局部组件状态到全局存储和服务端状态同步。
何时使用此技能
- 在 React 应用中设置全局状态管理
- 在 Redux Toolkit、Zustand 或 Jotai 之间进行选择
- 使用 React Query 或 SWR 管理服务端状态
- 实现乐观更新
- 调试与状态相关的问题
- 从传统 Redux 迁移到现代模式
核心概念
1. 状态分类
| 类型 | 描述 | 解决方案 |
|---|---|---|
| 局部状态 | 组件特有,UI 状态 | useState, useReducer |
| 全局状态 | 跨组件共享 | Redux Toolkit, Zustand, Jotai |
| 服务端状态 | 远程数据,缓存 | React Query, SWR, RTK Query |
| URL 状态 | 路由参数,搜索查询 | React Router, nuqs |
| 表单状态 | 输入值,验证 | React Hook Form, Formik |
2. 选择标准
小型应用,简单状态 → Zustand 或 Jotai
大型应用,复杂状态 → Redux Toolkit
大量服务端交互 → React Query + 轻量级客户端状态
原子化/细粒度更新 → Jotai
快速开始
Zustand (最简单的)
// 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' } // 存储名称
)
)
)
// 在组件中使用
function Header() {
const { user, theme, toggleTheme } = useStore()
return (
<header className={theme}>
{user?.name}
<button onClick={toggleTheme}>切换主题</button>
</header>
)
}
模式
模式 1: 使用 TypeScript 的 Redux Toolkit
// 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;
// 类型化的 hooks
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// store/slices/userSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
interface User {
id: string;
email: string;
name: string;
}
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;
模式 2: 使用 Slice 模式的 Zustand (可扩展)
// 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, // 组合后的 store 类型
[],
[],
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 });
// 可以访问其他 slice
// get().clearCart()
},
});
// 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),
}));
// 选择性订阅 (防止不必要的重渲染)
export const useUser = () => useStore((state) => state.user);
export const useCart = () => useStore((state) => state.cart);
模式 3: Jotai 用于原子状态
// atoms/userAtoms.ts
import { atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
// 基础原子
export const userAtom = atom<User | null>(null)
// 派生原子 (计算属性)
export const isAuthenticatedAtom = atom((get) => get(userAtom) !== null)
// 带 localStorage 持久化的原子
export const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light')
// 异步原子
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()
})
// 只写原子 (动作)
export const logoutAtom = atom(null, (get, set) => {
set(userAtom, null)
set(cartAtom, [])
localStorage.removeItem('token')
})
// 使用
function Profile() {
const [user] = useAtom(userAtom)
const [, logout] = useAtom(logoutAtom)
const [profile] = useAtom(userProfileAtom) // 支持 Suspense
return (
<Suspense fallback={<Skeleton />}>
<ProfileContent profile={profile} onLogout={logout} />
</Suspense>
)
}
模式 4: React Query 用于服务端状态
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
// 查询键工厂
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,
};
// 获取列表的 Hook
export function useUsers(filters: UserFilters) {
return useQuery({
queryKey: userKeys.list(filters),
queryFn: () => fetchUsers(filters),
staleTime: 5 * 60 * 1000, // 5 分钟
gcTime: 30 * 60 * 1000, // 30 分钟 (原 cacheTime)
});
}
// 获取单个用户的 Hook
export function useUser(id: string) {
return useQuery({
queryKey: userKeys.detail(id),
queryFn: () => fetchUser(id),
enabled: !!id, // 如果没有 id 则不获取
});
}
// 带有乐观更新的变更操作
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateUser, // 执行更新的函数
onMutate: async (newUser) => {
// 取消正在进行的相关查询的重新获取
await queryClient.cancelQueries({
queryKey: userKeys.detail(newUser.id),
});
// 获取并保存当前数据的快照,以便出错时回滚
const previousUser = queryClient.getQueryData(
userKeys.detail(newUser.id),
);
// 乐观地更新缓存
queryClient.setQueryData(userKeys.detail(newUser.id), newUser);
return { previousUser }; // 将快照传递给 onError 和 onSettled
},
onError: (err, newUser, context) => {
// 如果失败,根据保存的快照回滚
queryClient.setQueryData(
userKeys.detail(newUser.id),
context?.previousUser,
);
},
onSettled: (data, error, variables) => {
// 无论成功或失败,重新获取相关查询以确保数据与服务端同步
queryClient.invalidateQueries({
queryKey: userKeys.detail(variables.id),
});
},
});
}
模式 5: 结合客户端 + 服务端状态
// Zustand 用于客户端状态
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 用于服务端状态
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>
)
}
最佳实践
应该做的
- 就近管理状态 – 尽可能将状态放在离使用它的地方最近的位置
- 使用选择器 – 通过选择性订阅防止不必要的重新渲染
- 数据规范化 – 扁平化嵌套结构以便于更新
- 为所有内容添加类型 – 完整的 TypeScript 覆盖可以防止运行时错误
- 分离关注点 – 区分服务端状态 (React Query) 和客户端状态 (Zustand)
应避免的
- 不要过度全局化 – 并非所有状态都需要放入全局存储
- 不要重复服务端状态 – 让 React Query 来处理它
- 不要直接修改状态 – 始终使用不可变更新
- 不要存储派生数据 – 应该实时计算而非存储
- 不要混合范式 – 每个类别选择一个主要解决方案
迁移指南
从传统 Redux 迁移到 RTK
// 之前 (传统 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;
}
}
// 之后 (Redux Toolkit)
const todosSlice = createSlice({
name: "todos",
initialState: [],
reducers: {
addTodo: (state, action: PayloadAction<string>) => {
// Immer 允许“可变”的写法
state.push({ text: action.payload, completed: false });
},
},
});
📄 原始文档
完整文档(英文):
https://skills.sh/wshobson/agents/react-state-management
💡 提示:点击上方链接查看 skills.sh 原始英文文档,方便对照翻译。
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

评论(0)