React 状态管理完全指南:Context、Zustand、Jotai、Redux Toolkit、TanStack Query 深度选型与生产级实践

React 状态管理的完整体系:从 useState 到底层原理,Context 的适用与陷阱,Zustand 极简 Store,Jotai 原子化状态,Redux Toolkit 企业级方案,TanStack Query 服务端状态管理。按项目规模给出选型决策树、渲染性能分析和代码规范。

React 状态管理是前端开发中争议最多、选择最丰富的领域。从内置的 useState 到遍布社区的几十个状态库,选择困难的根本原因是状态本身具有多种形态,不同形态需要不同的管理方案。本文构建完整的 React 状态管理知识体系,从底层原理到生产实践,按项目规模给出明确的选型路径。


一、状态分类:先分清你要管什么

所有状态混乱的根源是不做分类就直接选型。React 应用中的状态分为四类:

状态类型示例特征管理方案
本地状态输入框值、模态框开关单个组件内使用useState / useReducer
共享状态用户信息、主题色多个组件需要读写Zustand / Jotai / Context
服务端状态API 返回的数据、缓存来源于服务器,需同步TanStack Query / SWR
URL 状态当前页码、筛选条件反映在用户地址栏React Router + URL 参数

核心原则:不要用全局状态库管理服务器直接提供的数据,也不要用本地 useState 管理跨组件的共享状态。选错层级是小项目变成状态泥沼的根本原因。

1.1 本地状态:useState vs useReducer

// useState:简单值或对象
const [count, setCount] = useState(0);

// useReducer:复杂状态转换逻辑
const [state, dispatch] = useReducer(reducer, initialState);
// dispatch({ type: 'increment', payload: 5 }) —— 类似 Redux 但局限在单个组件

useReducer 适用条件

  • 状态修改逻辑复杂(超过 3 种不同的修改方式)
  • 下一种状态依赖前一种状态(级联修改)
  • 需要集中测试状态转换逻辑(reducer 是纯函数,可独立单元测试)

1.2 Context 的真相:它不是状态管理工具

React Context 在官方文档中的定位是依赖注入机制,不是状态管理方案。它的本质是组件树级别的广播系统。

Context 的重渲染陷阱

// ❌ 反模式:把整个 store 放进一个 Context
function App() {
  const [state, setState] = useState({ user, theme, sidebar, notifications });
  return (
    <AppContext.Provider value={{ state, setState }}>
      <Layout />  {/* theme 变了,整个 Layout 会重渲染 */}
    </AppContext.Provider>
  );
}

Context 的问题在于:当 Provider 的 value 变化时,所有消费该 Context 的子组件都会重新渲染,无论它们实际依赖 value 中的哪个字段。

正确的 Context 用法:将 Context 拆分为垂直领域,每个 Context 只提供一类数据。

// ✅ 垂直拆分 Context,减少重渲染范围
function App() {
  const user = useUserState();
  const theme = useThemeState();
  return (
    <UserContext.Provider value={user}>
      <ThemeContext.Provider value={theme}>
        <Layout />
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

Context 适用场景总结

  • ✅ 配置/主题等低频率变更的数据(几个月变一次)
  • ✅ 依赖注入(如依赖倒置、测试 mock)
  • ❌ 高频变更的状态(表格数据、表单字段)
  • ❌ 大量状态字段(Context 的重渲染代价太高)

二、选型决策树:按项目规模匹配方案

项目规模 → 状态特征 → 推荐方案

[微项目]  < 10 个共享状态
  └── useState / useReducer 传递 + 垂直拆分 Context

[小项目] 10-30 个共享状态
  └── Zustand(极简,零样板代码)

[中项目] 30-60 个共享状态
  └── Zustand(模块化 slice)或 Jotai(原子化,按需订阅)

[大项目] 60+ 状态,多人协作
  └── Redux Toolkit(强规范、DevTools、时间旅行)+
      TanStack Query(服务端状态分离)

[服务端数据为主]
  └── TanStack Query(缓存、重取、乐观更新、去重请求)+
      Zustand(仅保留纯客户端状态)

三、Zustand:2025 年最推荐的入门方案

Zustand(德语“状态”)的设计理念是最少的 API,最强的能力。它不需要 Provider 包裹,没有样板代码,但支持状态派生、中间件、持久化等企业级特性。

3.1 基础用法

import { create } from 'zustand';

interface BearStore {
  bears: number;
  increase: () => void;
  decrease: () => void;
  reset: () => void;
}

const useBearStore = create<BearStore>((set, get) => ({
  bears: 0,
  increase: () => set((state) => ({ bears: state.bears + 1 })),
  decrease: () => set((state) => ({ bears: Math.max(0, state.bears - 1) })),
  reset: () => set({ bears: 0 }),
}));

// 组件中使用
function BearCounter() {
  const bears = useBearStore((state) => state.bears);
  return <h1>{bears} bears</h1>; // 只有 bears 变化时才重渲染
}

关键特性useBearStore(selector) 支持按字段订阅。上面的组件只在 bears 变化时重渲染,increase 方法变化不会触发它。

3.2 模块化 Slice 模式(中项目必用)

import { create } from 'zustand';

// 用户 slice
const createUserSlice = (set: any, get: any) => ({
  user: null as User | null,
  isLoggedIn: false,
  login: (user: User) => set({ user, isLoggedIn: true }),
  logout: () => set({ user: null, isLoggedIn: false }),
});

// 购物车 slice
const createCartSlice = (set: any, get: any) => ({
  items: [] as CartItem[],
  addItem: (item: CartItem) => {
    const { items } = get();
    set({ items: [...items, item] });
  },
  total: () => {
    const { items } = get();
    return items.reduce((sum, item) => sum + item.price * item.qty, 0);
  },
});

// 组合 store
const useStore = create((...args) => ({
  ...createUserSlice(...args),
  ...createCartSlice(...args),
}));

3.3 多 Store 模式 vs 单 Store 模式

模式适用代码组织
单 Store(Slices)< 30 个状态所有 slice 在一个文件或文件夹下
多 Store> 30 个状态,或状态间无关联useUserStoreuseCartStore 等独立
混合模式核心状态单 Store,独立模块多 Store主业务用单 Store,IM/通知等用独立 Store

3.4 持久化与中间件

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

const useUserStore = create(
  persist<UserState>(
    (set) => ({
      theme: 'dark',
      sidebarOpen: true,
      setTheme: (theme: string) => set({ theme }),
    }),
    {
      name: 'user-preferences',
      storage: createJSONStorage(() => localStorage),
      partialize: (state) => ({ theme: state.theme }), // 只持久化 theme
    }
  )
);

常用中间件

  • persist:localStorage/sessionStorage 持久化
  • devtools:Redux DevTools 集成
  • immer:不可变数据的突变式写法
  • subscribeWithSelector:精确订阅变化

四、Jotai:原子化状态与细粒度订阅

Jotai 由 react-spring 作者 Daishi Kato 开发,采用**原子(atom)**作为状态的基本单位。每个 atom 是独立的状态碎片,组件只订阅它需要的 atoms。

4.1 核心概念

import { atom, useAtom } from 'jotai';

// Primitive atoms:基础原子
const countAtom = atom(0);

// Derived atoms:派生原子(自动追踪依赖)
const doubleAtom = atom((get) => get(countAtom) * 2);

// Writable derived atoms
const tripleAtom = atom(
  (get) => get(countAtom) * 3,
  (get, set, newValue: number) => set(countAtom, newValue / 3)
);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const [double] = useAtom(doubleAtom); // 只读
  return (
    <div>
      <p>Count: {count}, Double: {double}</p>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
    </div>
  );
}

4.2 Jotai vs Zustand:什么时候选 Jotai?

维度ZustandJotai
心智模型集中式 Store(类 Redux)分布式原子(类 Recoil)
API 数量极少原子 + hooks
TypeScript良好极佳(原子自带类型)
按需订阅✅ Selector✅ 自动追踪依赖
派生状态手动实现✅ atom(get) 自动派生
异步状态✅ 支持✅ 支持
生态大小大(中间件丰富)中等

推荐:如果你习惯于每个组件自己定义和管理 atoms,或者需要大量细粒度的状态组合和派生,选 Jotai。如果你偏好明确的 store 结构,选 Zustand。

4.3 Jotai 高级模式

// 异步原子(数据获取)
const postsAtom = atom(async () => {
  const res = await fetch('/api/posts');
  return res.json();
});

// 带有 write 的原子
const updatePostAtom = atom(
  null,
  async (get, set, { id, data }: { id: string; data: PostData }) => {
    const res = await fetch(`/api/posts/${id}`, {
      method: 'PATCH',
      body: JSON.stringify(data),
    });
    const updated = await res.json();
    set(postsAtom, (prev) => prev.map((p) => (p.id === id ? updated : p)));
  }
);

// Provider scope(隔离状态,用于测试或 SSR)
function PostEditor({ id }: { id: string }) {
  return (
    <Provider>
      <EditorContent id={id} />
    </Provider>
  );
}

五、Redux Toolkit:企业级首选

Redux 因样板代码过多而备受诟病,Redux Toolkit(RTK)是官方推荐的现代 Redux 写法,将 actions、reducers 和 middleware 压缩为简洁的 API。

5.1 RTK 基础:createSlice

import { createSlice, configureStore, createAsyncThunk } from '@reduxjs/toolkit';

// 异步 thunk
const fetchUser = createAsyncThunk('user/fetch', async (userId: string) => {
  const res = await fetch(`/api/users/${userId}`);
  return res.json();
});

const userSlice = createSlice({
  name: 'user',
  initialState: { user: null, loading: false, error: null } as UserState,
  reducers: {
    clearUser: (state) => { state.user = null; },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => { state.loading = true; })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.user = action.payload;
        state.loading = false;
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.error = action.error.message;
        state.loading = false;
      });
  },
});

const store = configureStore({
  reducer: { user: userSlice.reducer },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export { fetchUser, clearUser };

5.2 Redux 为什么还是企业级首选?

企业级需求Redux 的优势
可预测性单一 Store,所有状态变更通过 action,完全可追踪
调试Redux DevTools 支持时间旅行、action replay、状态快照
测试Reducer 是纯函数,输入输出确定,易于单元测试
规范强制的 action-reducer 结构,团队代码风格一致
中间件生态redux-saga、redux-observable 等处理复杂副作用
持久化redux-persist 成熟稳定

实际经验:如果你的团队超过 5 个前端开发者,或者状态逻辑涉及多个模块的联动操作,Redux Toolkit 的规范性带来的收益会超过 Zustand 的简洁性。

5.3 RTK Query:RTK 内的 TanStack Query 替代方案

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getPosts: builder.query<Post[], void>({ query: () => '/posts' }),
    getPost: builder.query<Post, string>({ query: (id) => `/posts/${id}` }),
    updatePost: builder.mutation<Post, { id: string; body: PostData }>({
      query: ({ id, body }) => ({
        url: `/posts/${id}`,
        method: 'PATCH',
        body,
      }),
    }),
  }),
});

export const { useGetPostsQuery, useGetPostQuery, useUpdatePostMutation } = api;

RTK Query 优势:与 Redux 深度集成,自动缓存、自动重取、乐观更新、去重,无需在 Redux + TanStack Query 之间切换。


六、TanStack Query:服务端状态的第一选择

服务端状态(Server State)管理是所有 React 应用中最大的误区。大多数开发者会把 API 返回的数据存进全局状态库(Redux/Zustand),然后手动写缓存、重取、错误处理,反而更复杂。

TanStack Query(前身为 React Query)是专门管理服务端状态的库,它把数据获取抽象为第一公民。

6.1 核心概念

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// 查询
const { data, isLoading, error, isFetching } = useQuery({
  queryKey: ['posts', postId],
  queryFn: () => fetchPost(postId),
  staleTime: 1000 * 60 * 5,      // 5分钟内数据不会重新获取
  gcTime: 1000 * 60 * 30,        // 缓存保留30分钟(v5 参数名)
  retry: 3,                       // 失败后重试3次
  refetchOnWindowFocus: true,     // 窗口聚焦时自动重取
});

queryKey 的设计原则:队列数组,第一个元素是领域,后续是参数。['posts']['posts', 'draft']['posts', { status: 'published', page: 1 }]

6.2 乐观更新(Optimistic Updates)

const queryClient = useQueryClient();

const mutation = useMutation({
  mutationFn: updatePost,
  onMutate: async (newPost) => {
    // 1. 取消正在进行的重新获取
    await queryClient.cancelQueries({ queryKey: ['posts', newPost.id] });

    // 2. 保存之前的状态用于回滚
    const previousPost = queryClient.getQueryData(['posts', newPost.id]);

    // 3. 乐观更新 UI
    queryClient.setQueryData(['posts', newPost.id], newPost);

    return { previousPost };
  },
  onError: (err, newPost, context) => {
    // 4. 发生错误时回滚
    queryClient.setQueryData(['posts', newPost.id], context?.previousPost);
  },
  onSettled: (newPost) => {
    // 5. 最终数据同步
    queryClient.invalidateQueries({ queryKey: ['posts', newPost?.id] });
  },
});

6.3 服务端状态 vs 客户端状态分离策略

// ✅ 推荐:分离策略
// 服务端状态:只用 TanStack Query
const { data: user } = useQuery({ queryKey: ['user'], queryFn: fetchUser });
const { mutate: updateUser } = useMutation({ mutationFn: updateUserApi });

// 客户端状态:Zustand 或 Context
const theme = useStore((state) => state.theme);
const toggleSidebar = useStore((state) => state.toggleSidebar);

为什么分离?

  • JavaScript(所有全局状态库)没有内置缓存失效策略,而 TanStack Query 的缓存失效是核心能力
  • Redux/Zustand 管理服务端数据需要手动写大量样板代码(loading、error、重取、乐观更新)
  • 分离后服务端状态的测试由 API 测试覆盖,客户端状态由单元测试覆盖,边界清晰

七、性能对比表

重渲染优化内存占用初始加载体积DevTools
Context❌ 无(全树重渲染)极小内置React DevTools
Zustand✅ Selector 精确订阅极小~1KB可选 Redux DevTools
Jotai✅ 原子级订阅极小~3KB无(依赖 React DevTools)
Redux Toolkit✅ connect/useSelector~11KB(含 RTK Query)Redux DevTools
TanStack Query✅ 缓存+去重~12KBReact Query DevTools

八、生产级代码规范

8.1 Store 文件组织

stores/
├── index.ts          # store 导出
├── userStore.ts      # 用户 slice(Zustand)
├── cartStore.ts      # 购物车 slice
├── types.ts          # 共享类型
└── middleware/       # 自定义中间件

8.2 状态命名约定

// ✅ 使用语义化命名,避免简写
interface UserState {
  isLoading: boolean;        // 异步操作进行中
  isAuthenticated: boolean;  // 认证状态
  currentUser: User | null;  // 当前用户信息
  error: Error | null;       // 错误对象
}

// ❌ 避免
interface BadState {
  loading: boolean;          // 语义弱
  errorMessage: string;      // 应该统一用 error: Error
  data: any;                 // 具体类型不明
}

8.3 不要一切都全局化

// ❌ 反模式:把 Form 状态塞进全局 Store
const useBadStore = create(() => ({
  loginForm: { username: '', password: '' },  // 只在 Login 页使用
}));

// ✅ 正确:Form 状态留在组件内
function LoginPage() {
  const [form, setForm] = useState({ username: '', password: '' });
  // 只在需要提交时才与全局状态交互
}

常见问题(FAQ)

Context、Zustand、Redux 怎么选?

  • 小项目(< 5 状态):Zustand(简单,性能好)
  • 中型项目(5-20 状态):Zustand(模块化 slice)或 Jotai(原子化)
  • 大型团队(> 5 人):Redux Toolkit(规范强,DevTools 完善,action 可追溯)
  • 服务端数据为主:TanStack Query + 少量 Zustand(客户端状态和 URL 状态)

为什么有了 TanStack Query 还需要状态管理库?

TanStack Query 管理服务端状态(API 数据),但客户端状态(主题、模态框、表单草稿、侧边栏展开)仍需要 Zustand/Redux。两者不是竞争关系,是互补关系。理想架构是:服务端状态交给 TanStack Query,只把纯客户端状态放进全局 Store。

Zustand 和 Jotai 到底哪个好?

两者都轻量、TypeScript 友好、按需订阅。Zustand 更像 Redux(集中式 store,直接修改);Jotai 更像 Recoil(分布式原子,组合派生)。如果团队已有 Redux 经验选 Zustand,如果喜欢函数式组合选 Jotai,实际性能差异可忽略。

Redux Toolkit 还需要中间件吗?

RTK 内置 Redux Thunk。如果需要复杂副作用流(如竞态处理、debounce、轮询),推荐 RTK Query 或 redux-saga。小型异步直接用 createAsyncThunk

相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章