🚀 快速安装

复制以下命令并运行,立即安装此 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 原始英文文档,方便对照翻译。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。