React 状态管理

从 Redux 核心到 Zustand 轻量方案,再到 Jotai 原子化和 TanStack Query 数据获取——按状态类型和团队规模判断如何选择状态管理方案。

React 状态管理

概述

React 的内置状态机制——useStateuseReducer——足以应付中小型应用的局部状态。但当应用复杂度上升,状态可能在多个不相关组件间共享、需要持久化、需要支持撤销/重做、或者需要管理异步数据获取时,我们就需要更系统化的状态管理方案。

状态管理的核心问题是:状态应该放在哪里?谁应该拥有它?如何让需要它的组件方便地访问?

这个问题没有 universally correct answer。不同规模、不同团队、不同业务场景下,最优方案差异很大。理解各方案的设计哲学和 trade-off,比记住"某个场景用 X"更重要。


状态分类框架

在讨论具体方案之前,我们需要先理解状态的分类。因为不同类型的状态,适合用不同的管理方案

Dan Abramov 提出的 You Might Not Need a Tracking Library 一文中,对状态分类有精辟论述:

Think about what makes state "local" — does the component use it and nothing else? Then keep it in useState or useReducer. Only move it somewhere else when you genuinely need to share it.

四类状态

状态类型 特征 存储位置
UI 状态 仅影响单个组件渲染 useState / useReducer
跨组件状态 多个相关组件共享 Context / 状态管理库
服务端状态 从 API 获取,会过期,需要缓存 TanStack Query / SWR
持久化状态 跨会话保存(localStorage、URL) 专门的持久化方案

关键洞察:服务端状态是最容易被忽视的一类。很多时候所谓的"状态管理问题",本质上是"数据获取和缓存问题"。


Redux 核心概念

设计哲学

Redux 遵循三大原则:

  1. Single Source of Truth:整个应用的 state 存储在一个只读的 store 中
  2. State is Read-Only:不能直接修改 state,必须通过 dispatch action 来描述"发生了什么"
  3. Reducers are Pure:用纯函数计算新状态,保证可预测性

这种架构叫做 Unidirectional Data Flow(单向数据流)

核心概念:Store

Store 是 Redux 的核心对象,它:

  • 持有应用的完整 state 树
  • 提供 getState() 读取状态
  • 提供 dispatch(action) 更新状态
  • 提供 subscribe(listener) 注册监听器
import { createStore } from 'redux';

const store = createStore(rootReducer);

store.subscribe(() => {
  console.log('State changed:', store.getState());
});

store.dispatch({ type: 'INCREMENT' });
// State changed: { counter: 1 }

核心概念:Reducer

Reducer 是一个纯函数,接收当前状态和 action,返回新状态:

const counterReducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return { count: 0 };
    default:
      return state;
  }
};

Redux 的核心约束:Reducers 必须是纯函数。

  • 相同输入 → 相同输出
  • 不应有副作用(API 调用、Mutation)
  • 不应调用非纯函数(如 Date.now()

核心概念:Action

Action 是一个普通对象,描述"发生了什么":

// Action Creator
const increment = () => ({ type: 'INCREMENT' });
const addTodo = (text) => ({
  type: 'ADD_TODO',
  payload: { id: Date.now(), text, completed: false }
});

// Dispatch
store.dispatch(increment());
store.dispatch(addTodo('Learn Redux'));

Action 是数据的唯一入口。好处是所有状态变更都被记录下来,支持时间旅行调试、撤销/重做等高级功能。

Middleware:扩展 Redux

Middleware 是 Redux 的扩展机制,位于 dispatch 和 reducer 之间,可以:

  • 处理异步逻辑
  • 记录日志
  • 路由集成
  • 任何 side effect
// 自定义 middleware 签名
const logger = (storeAPI) => (next) => (action) => {
  console.log('Before:', storeAPI.getState());
  let result = next(action);  // 继续传递 action
  console.log('After:', storeAPI.getState());
  return result;
};

// 应用 middleware
import { applyMiddleware } from 'redux';
const store = createStore(rootReducer, applyMiddleware(logger, thunk));

最常用的 middleware

  • redux-thunk:支持异步 action creator
  • redux-saga:使用 Generator 处理复杂异步流程
  • redux-logger:开发环境日志
  • redux-toolkit:Redux 官方推荐的现代 Redux 编写方式

Redux Toolkit:现代 Redux 写法

Redux Toolkit(RTK)是官方推荐的 Redux 编写方式,解决了"样板代码过多"的问题:

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

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => {
      // RTK 允许直接修改 state(内部使用 Immer)
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
  },
});

export const { increment, decrement } = counterSlice.actions;

const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
  },
});

// 自动生成 action creators
store.dispatch(increment());
console.log(store.getState().counter.value); // 1

RTK 的核心改进

  1. createSlice:自动生成 action creators 和 action types
  2. Immer:允许"可变"语法书写不可变更新
  3. configureStore:简化 store 配置,自动添加 middleware
  4. RTK Query:内置的数据获取和缓存方案

Redux 的适用场景

Redux 并不是万能的。它的优势在于:

  • 复杂的中大型应用
  • 需要时间旅行调试
  • 需要撤销/重做
  • 状态变更需要可追溯
  • 团队需要统一的状态管理模式

Redux 的不足

  • 样板代码仍然较多(RTK 改善了这一点)
  • 需要额外学习成本(actions、reducers、middleware)
  • 对于简单场景过于重量

Zustand:轻量级方案

设计理念

Zustand 是一个轻量级的状态管理库,核心设计理念是:状态管理不应该复杂

与 Redux 相比,Zustand 没有 Provider、没有 action/reducer 分离、没有 middleware 概念。状态、更新逻辑、订阅机制都在一个 store 定义中。

import { create } from 'zustand';

const useStore = create((set) => ({
  bears: 0,
  increase: () => set((state) => ({ bears: state.bears + 1 })),
  reset: () => set({ bears: 0 }),
}));

// 在组件中使用
function BearCounter() {
  const bears = useStore((state) => state.bears);
  const increase = useStore((state) => state.increase);
  return (
    <div>
      <p>{bears} bears</p>
      <button onClick={increase}>Increase</button>
    </div>
  );
}

Zustand 的核心特性

1. 去中心化 Store

Redux 推荐单一 store,而 Zustand 允许创建多个独立的 store:

const useAuthStore = create((set) => ({
  user: null,
  login: (user) => set({ user }),
  logout: () => set({ user: null }),
}));

const useCartStore = create((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
}));

2. 精确订阅

Zustand 的 selector 机制确保只有变化的状态才会触发重新渲染

// 只有 bears 变化才重新渲染
const bears = useStore((state) => state.bears);

// 使用 shallow 比较数组
const { bears, fishes } = useStore(
  (state) => ({ bears: state.bears, fishes: state.fishes }),
  shallow
);

3. 开发者体验

  • TypeScript 支持良好
  • 内置 subscribeWithSelector middleware
  • 支持持久化(通过 plugin)
  • 支持 React 外部使用(vanilla store)

Zustand vs Redux

维度 Zustand Redux
包大小 ~1KB ~7KB(不含 middleware)
概念复杂度
Boilerplate 极少 较多(RTK 改善)
DevTools 支持 完整支持
中间件 插件化 middleware
适用规模 中小型 中大型

Jotai:原子化状态管理

设计理念

Jotai 的核心概念是原子(Atom)。每个 atom 是一个独立的最小状态单元,组件直接订阅需要的 atom,而不是订阅整个 store。

import { atom, useAtom } from 'jotai';

// 基础 atom
const countAtom = atom(0);

// 派生 atom(类似 computed)
const doubledAtom = atom((get) => get(countAtom) * 2);

// 在组件中使用
function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const [doubled] = useAtom(doubledAtom);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}

原子化的优势

1. 精确重渲染

Jotai 使用 atom 作为重渲染的最小单元。只有订阅的 atom 变化才会触发更新:

// 组件 A 只订阅 countAtom
function ComponentA() {
  const [count] = useAtom(countAtom);  // 只依赖 countAtom
}

// 组件 B 只订阅 nameAtom
function ComponentB() {
  const [name] = useAtom(nameAtom);  // 只依赖 nameAtom
}

// countAtom 变化时,只有 ComponentA 重渲染

2. 派生状态自动计算

派生 atom 只有在被至少一个组件订阅时才会重新计算:

const expensiveAtom = atom((get) => {
  // 只有被使用时才会执行
  return computeExpensiveValue(get(basicAtom));
});

3. 异步支持

const userDataAtom = atom(async (get) => {
  const userId = get(userIdAtom);
  return fetchUser(userId);
});

function UserProfile() {
  // Jotai 自动处理 Promise
  const [user] = useAtom(userDataAtom);

  if (user === undefined) return <Skeleton />;
  if (user === null) return <Error />;
  return <div>{user.name}</div>;
}

Jotai vs Zustand vs Redux

维度 Jotai Zustand Redux
状态单元 Atom(最小单元) Store(可多个) Single Store
重渲染粒度 Atom 级别 Store 级别 Store 级别
学习曲线 低-中
派生状态 内置 atom 手动 selector selector/mapState
异步 原生支持 需 middleware 需 middleware
适用场景 细粒度状态共享 中小型应用 中大型应用

TanStack Query:服务端状态管理

问题背景

服务端状态(从 API 获取的数据)与客户端状态有本质不同:

特性 客户端状态 服务端状态
更新频率 由用户交互决定 由服务器数据决定
持久性 通常是临时的 应该是持久的
同步性 总是本地最新 可能与服务器不同步
获取方式 直接读写 需要异步获取
缓存 不需要 需要

核心问题:传统的状态管理库(Redux、Zustand、Jotai)都不是为服务端状态设计的。用它们管理 API 数据,要么自己实现缓存/失效逻辑,要么产生大量样板代码。

TanStack Query(原 React Query)解决方案

TanStack Query 是专门为服务端状态设计的库,核心功能:

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

function UserProfile({ userId }) {
  // 自动处理:加载状态、错误、缓存、重新获取
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 5 * 60 * 1000,  // 5 分钟内不重新获取
  });

  if (isLoading) return <Skeleton />;
  if (error) return <Error error={error} />;
  return <div>{user.name}</div>;
}

// 数据修改后自动使缓存失效
function UpdateUser() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (newUser) => updateUser(newUser),
    onSuccess: () => {
      // 清除相关缓存,触发重新获取
      queryClient.invalidateQueries({ queryKey: ['user'] });
    },
  });

  return (
    <button onClick={() => mutation.mutate({ name: 'New Name' })}>
      Update
    </button>
  );
}

TanStack Query 的核心概念

1. Query:数据获取

const { data, isLoading, isError, error, refetch } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  enabled: !!userId,  // 条件执行
  select: (data) => data.filter(t => t.completed),  // 数据转换
});

2. Mutation:数据修改

const mutation = useMutation({
  mutationFn: createTodo,
  onSuccess: (newTodo) => {
    // Optimistic update 或缓存更新
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
  },
  onError: (error, variables, context) => {
    // 错误处理,回滚操作
  },
});

3. 缓存策略

// 缓存自动清理配置
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,     // 数据新鲜时间
      gcTime: 10 * 60 * 1000,         // 缓存回收时间
      refetchOnWindowFocus: true,     // 窗口聚焦时重新获取
      retry: 3,                       // 失败重试次数
    },
  },
});

TanStack Query vs Redux

维度 TanStack Query Redux
设计目标 服务端状态 通用状态
缓存管理 内置 需手动实现
重新获取 自动 需手动触发
加载状态 内置 需手动管理
错误处理 内置 需手动管理
API 响应 原始数据 处理后的数据

选择决策框架

按状态类型选择

状态类型
├── 组件本地状态
│   └── useState / useReducer
├── 跨组件共享的简单状态
│   └── Context
├── 需要高性能细粒度更新的状态
│   └── Jotai
├── 中小型应用的状态共享
│   └── Zustand
├── 复杂中大型应用的状态管理
│   └── Redux Toolkit
├── 服务端数据(API 响应)
│   └── TanStack Query
└── 需要持久化的状态
    └── Zustand + persist / Redux + redux-persist

按团队规模选择

小型团队 / 简单项目

  • 优先用 useState + useReducer
  • 需要共享时用 Context
  • 服务端数据直接 fetch,状态简单
  • 过度设计是最大的敌人

中型团队 / 中等复杂度项目

  • Zustand 作为主力状态管理
  • TanStack Query 处理服务端状态
  • 避免 Redux 的过度复杂性
  • 平衡简洁性和可扩展性

大型团队 / 复杂项目

  • Redux Toolkit 提供规范性和可追溯性
  • TanStack Query 处理所有数据获取
  • 明确的 action 命名和 reducer 模式
  • 时间旅行调试和状态追溯的价值显现

混合使用是常态

在实际项目中,很少只用一个方案

// 组合使用示例
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <PersistGate persistor={persistor}>
        <Provider store={store}>
          <Router />
        </Provider>
      </PersistGate>
    </QueryClientProvider>
  );
}
  • TanStack Query:服务端状态
  • Redux Toolkit:全局客户端状态(用户信息、UI 状态)
  • Zustand:轻量级独立模块(购物车、播放器)
  • useState:组件本地临时状态

常见问题与解决方案

问题 1:Context 导致的额外渲染

Context 的问题是:任何 Context 值变化,所有消费组件都会重新渲染

解决方案:

// 1. 拆分 Context
const UserContext = createContext(null);
const ThemeContext = createContext(null);

// 2. 使用 useContextSelector(jotai/react-query 方案)
const user = useUser();  // 只订阅 user 变化

问题 2:状态同步 vs 状态管理

很多"状态管理问题"本质上是状态同步问题

// 错误:在两个地方存储相同的数据
const [user, setUser] = useState(null);
localStorage.setItem('user', JSON.stringify(user));

// 正确:单一数据源 + 持久化层处理同步
// 状态管理库负责状态,持久化库负责同步

问题 3:API 状态与 UI 状态混合

// 错误:把 API 响应存在 Redux 里
const initialState = {
  users: [],       // API 数据
  loading: false,  // UI 状态
  error: null,     // UI 状态
  selectedId: null // UI 状态
};

// 正确:分离关注点
// TanStack Query 管理 users、loading、error
// Redux(或其他库)只管 selectedId

延展阅读