React:聊一聊状态管理

229 阅读11分钟

react-banner.png

😀 前言

众所周知,状态管理是 React 应用中绕不开的重要部分, 从早期的ReduxMobox再到新生代的Zustand以及Jotai,甚至因为React在2020年12月底 推出的RSC - React Server Components , React 将原来所有的客户端组件根据依赖划分成了

  • 客户端组件 (依赖状态, 需要使用State,props, hooks等传统组件,如 UI 控件的开关状态)
  • 服务端组件 (依赖数据源,数据库、GraphQL端点或 fs 文件系统 等,这类状态有缓存、失效、重新获取、分页等独特需求)

至此催生了React Query/SWRRTK Query这类服务端状态管理库。我们作为开发者是不是有一种我不禁想问的冲动?为什么会出现这么多的状态管理库加重使用者的学习成本以及心智负担?为什么官方不能像Vue一样推出Vuex这种一统江湖的状态管理库?带着这些问题,接下来我将带大家了解一下React 状态管理库.


🙋 React 为什么需要状态管理?

React 的核心是“状态驱动视图”:UI = f(state)。组件自身的 state 和 props 足以管理简单的、局部的状态。但随着应用复杂度提升,仅凭它们会遇到诸多问题:

  1. Props Drilling(属性钻取) :当多个不同层级的组件需要共享同一个状态时,你必须通过 props 将状态一层层向下传递,即使中间层组件完全不需要这个状态。这导致组件耦合度增高,难以维护。

  2. 状态提升(State Lifting)的负担:共享状态需要提升到最近的公共父组件中。这可能导致根组件的状态变得无比庞大,任何对状态结构的修改都可能引发连锁反应。

  3. 兄弟组件通信困难:两个非父子关系的组件要通信,必须通过共同的祖先组件,通过“状态提升” + “回调函数传递”的方式,过程繁琐。

  4. 状态同步问题:同一个状态可能被多个分散的组件修改,逻辑散落在各处,难以追踪和调试,容易产生不一致。

  5. 性能优化困境:将状态提升到很高的父组件后,任何该状态的微小变化都会导致整个大的父组件树重新渲染。虽然React有高效的Diff算法,但不必要的重新渲染仍然存在性能开销。你需要精细地使用 React.memouseMemouseCallback 来优化,这本身就增加了开发和心智负担。理想的状态管理库应该能提供更精细化的订阅机制,只让依赖了特定状态变化的组件重新渲染。

哪些隐式需求导致状态管理出现的必然性?

  • 可预测性(Predictability) :状态的变化应该有一个清晰、固定的流程,以便容易预测和理解应用的行为。
  • 可维护性(Maintainability) :状态逻辑应该可以被集中管理、复用和独立测试,而不是散落在各个组件的 useState 和 useEffect 中。
  • 可调试性(Debuggability) :需要有能力追踪每一次状态变化的来源、原因和具体过程(如 Redux DevTools)。
  • 性能(Performance) :避免不必要的渲染。当状态更新时,只有真正依赖该状态的组件才应该重新渲染。
  • 解耦(Decoupling) :组件应该更多地关注于渲染 UI,而不是如何获取和管理它们所需的状态。

这些需求催生了专门的状态管理库,它们提供了一个在 React 组件树之外的“外部状态”容器,组件可以按需订阅和消费其中的状态,从而解决上述问题。


🕵 状态管理库介绍

上面我们了解到了状态管理库出现的原因以及背后的需求。但由于React社区高度创新且自由,所以你懂得,React的状态管理库多的眼花缭乱,已经无力抉择了... 如果想要全部掌握,学习成本颇高,所以我尝试将各种状态库的核心模型:【状态容器、读取、更新、订阅】抽象提炼出来帮助大家快速学习并理解.

看似学习成本高,但实际上这些库的核心概念是相通的。一旦理解了 状态容器、读取、更新、订阅 这个基本模型,学习任何一个新库都会非常快。

Redux:单向数据流的典范

Redux 的核心是单一不可变状态树和纯函数更新器,遵循严格的单向数据流。(繁琐的样板代码)

// 1. 定义 Action Types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// 2. 创建 Action Creators
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });

// 3. 定义 Reducer - 纯函数,是更新的核心
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case INCREMENT:
      return state + 1; // 关键:返回新状态,不修改原状态
    case DECREMENT:
      return state - 1;
    default:
      return state;
  }
};

// 4. 创建 Store - 状态容器
const { createStore } = Redux;
const store = createStore(counterReducer);

// 5. 订阅状态变化
const unsubscribe = store.subscribe(() => {
  console.log('State changed:', store.getState());
});

// 6. 分发 Action 来更新状态
store.dispatch(increment()); // State changed: 1
store.dispatch(increment()); // State changed: 2
store.dispatch(decrement()); // State changed: 1

// 7. 取消订阅
unsubscribe();

内核总结

  • 容器createStore(reducer) 创建的单一 store
  • 读取store.getState() 直接获取状态
  • 更新: 定义纯函数 reducer,通过 store.dispatch(action) 触发
  • 订阅store.subscribe(callback) 订阅变化

你不会是唯一一个觉得 Redux 难用的人, 因为官方也觉得 React-Redux 难用,所以推出了 Redux Toolkit(RTK) 来简化 Redux的操作

MobX:响应式编程的代表

响应式编程(Reactive Programming)可变状态(Mutable State) 。它将状态包装为可观察对象(Observable),任何对状态的修改都会自动触发所有依赖该状态的计算(Computed)和副作用(Reaction)。理念是“让状态管理变得透明和自动化”,更符合面向对象思维。

// 1. 定义可观察状态 - 状态容器
const { makeAutoObservable } = mobx;

class CounterStore {
  count = 0; // 状态本身

  constructor() {
    makeAutoObservable(this); // 魔法核心:使属性和方法可观察
  }

  // Action - 更新状态的方法
  increment() {
    this.count++; // 直接"可变"修改!
  }

  decrement() {
    this.count--;
  }
}

const counterStore = new CounterStore();

// 2. 创建响应式副作用
const { autorun } = mobx;

// autorun 自动追踪依赖并在变化时重新执行
const dispose = autorun(() => {
  console.log('Count is:', counterStore.count);
});

// 3. 触发更新
counterStore.increment(); // Count is: 1
counterStore.increment(); // Count is: 2
counterStore.decrement(); // Count is: 1

// 4. 停止响应
dispose();

内核总结

  • 容器: 普通类实例,使用 makeAutoObservable 处理
  • 读取: 直接读取属性,自动被追踪
  • 更新: 直接调用方法修改状态
  • 订阅autorun 自动建立依赖关系

Zustand:极简的 Hook 式方案

简单、不固执己见(Unopinionated)  。它吸收了 Redux 和 MobX 的优点,摒弃了它们的繁琐。核心是一个极简的不可变状态管理器,使用 Hook 作为主要 API。没有模板代码,没有 Provider 包裹层,API 非常直观。

// 1. 创建 Store - 状态容器
const { create } = zustand;

const useCounterStore = create((set, get) => ({
  count: 0,
  
  // 更新方法
  increment: () => set(state => ({ count: state.count + 1 })),
  decrement: () => set(state => ({ count: state.count - 1 })),
  
  // 异步操作示例
  incrementAsync: () => {
    setTimeout(() => {
      set(state => ({ count: state.count + 1 }))
    }, 1000);
  },
  
  // 获取当前状态
  getCount: () => get().count,
}));

// 2. 在组件外使用
console.log(useCounterStore.getState().getCount()); // 0

// 订阅状态变化
const unsubscribe = useCounterStore.subscribe((state) => {
  console.log('Count changed:', state.count);
});

// 3. 触发更新
useCounterStore.getState().increment(); // Count changed: 1
useCounterStore.getState().increment(); // Count changed: 2
useCounterStore.getState().decrement(); // Count changed: 1

// 4. 取消订阅
unsubscribe();

内核总结

  • 容器create 函数创建的存储
  • 读取useStore Hook 或 store.getState()
  • 更新: 使用 set 函数进行不可变更新
  • 订阅store.subscribe 或 Hook 内部自动处理

Recoil:原子状态管理

Recoil 使用原子(Atom)和选择器(Selector)管理状态,适合复杂状态依赖。

// 1. 定义原子 - 状态单位
const { atom, useRecoilState, useRecoilValue } = Recoil;

const countState = atom({
  key: 'countState', // 全局唯一ID
  default: 0, // 默认值
});

// 派生状态示例
const doubledCountState = selector({
  key: 'doubledCountState',
  get: ({ get }) => get(countState) * 2,
});

// 2. 在React组件中使用
function Counter() {
  const [count, setCount] = useRecoilState(countState);
  const doubledCount = useRecoilValue(doubledCountState);

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

// 3. 在非组件环境中访问(需通过Recoil的API)
// 注意:Recoil设计上主要与React集成,非组件环境使用较复杂

内核总结

  • 容器: 分散的 Atom 和 Selector
  • 读取useRecoilState 和 useRecoilValue Hooks
  • 更新: 使用 setter 函数(类似 useState)
  • 订阅: Hook 自动按原子订阅

Jotai:Primitive原子状态

原子式状态管理(Atomic State Management)  ,受 Recoil 启发。状态被拆分为一个个最小的原子(atom),组件通过读取原子来创建依赖关系。只有原子的值改变时,依赖它的组件才会重新渲染。它完美契合 React 的并发模式(Concurrent Mode),致力于解决衍生状态(derived state)问题。

// 1. 定义原子
const { atom, useAtom } = jotai;

const countAtom = atom(0); // 基本原子
const doubledAtom = atom((get) => get(countAtom) * 2); // 派生原子

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

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

// 3. 在非组件环境中使用(需要创建Store)
const { createStore } = jotai;
const store = createStore();

store.set(countAtom, 5);
console.log(store.get(countAtom)); // 5
console.log(store.get(doubledAtom)); // 10

内核总结

  • 容器: 原子(atom)是基本单位
  • 读取useAtom Hook 或 Store 的 get 方法
  • 更新: 使用 setter 函数或 Store 的 set 方法
  • 订阅: Hook 或 Store 自动处理订阅

Valtio:基于代理的响应式状态

Valtio 使用 Proxy 实现响应式状态,API 极其简洁。

// 1. 创建状态代理 - 状态容器
const { proxy, useSnapshot, subscribe } = valtio;

const state = proxy({
  count: 0,
  // 可以嵌套复杂对象
  user: {
    name: "John",
    age: 30
  }
});

// 2. 订阅状态变化
const unsubscribe = subscribe(state, () => {
  console.log('State changed:', state.count);
});

// 3. 直接修改状态
state.count++; // State changed: 1
state.count++; // State changed: 2
state.user.age = 31; // 深度更新也响应

// 4. 在React组件中使用
function Counter() {
  const snap = useSnapshot(state); // 创建状态的不可变快照

  return (
    <div>
      <p>Count: {snap.count}</p>
      <button onClick={() => state.count++}>+</button>
      <button onClick={() => state.count--}>-</button>
    </div>
  );
}

// 5. 取消订阅
unsubscribe();

内核总结

  • 容器proxy 创建的响应式代理对象
  • 读取: 直接访问属性或通过 useSnapshot Hook
  • 更新: 直接修改代理对象的属性
  • 订阅subscribe 函数订阅变化

对比总结

范式状态组织更新方式订阅机制特点
Redux函数式单一存储dispatch → reducersubscribe严格、可预测、模板代码多
MobX响应式可观察对象直接修改autorun 自动追踪直观、代码少、黑魔法多
Zustand函数式Hook存储set函数自动选择订阅极简、轻量、少模板代码
Recoil原子式分散原子setter函数按原子订阅Meta官方、适合复杂依赖
Jotai原子式原始原子setter函数自动订阅Recoil简化版、更原始
Valtio响应式代理对象直接修改subscribe基于Proxy、API极简

🙅 为什么 React 官方不出一个官方状态管理库?

这是一个社区经常讨论的问题。核心原因在于 React 团队的设计哲学

  • “灵活的抽象”React 本身是一个视图库,它故意不规定你如何管理状态(无论是客户端还是服务端)。这种“不固执己见”使得社区能涌现出 Redux, MobX, Zustand, React Query 等各种优秀方案,它们针对不同场景和偏好进行优化。官方一旦推出,可能会抑制这种创新。

  • “核心职责” :React 团队更专注于 React 本身的核心能力(如并发渲染、服务器组件、编译器优化),他们倾向于提供底层的、稳定的原始 API(如 ContextuseStateuseReducer),让社区在此基础上自由创新和探索最佳实践,而不是维护一个可能非常复杂且众口难调的状态管理库。他们认为状态管理应该由社区来解决。

  • Context 不是为此而生:很多人误以为 Context 是状态管理工具。React 团队多次强调,Context 是依赖注入机制,而不是状态管理。它本身没有状态更新优化能力,当 Context 的值变化时,所有消费该 Context 的组件都会重新渲染,无论它们是否依赖于变化的那部分数据。这对于频繁变化的全局状态来说是性能灾难。真正的状态管理库(包括Zustand、Jotai)都使用了更细粒度的订阅机制。

  • 成功的先例并不绝对Vuex/Pinia 的成功确实得益于其官方地位,但这并不意味着这是唯一正确的路径。React 生态的繁荣恰恰证明了多元化竞争的优势。

结语React 官方选择提供底层的能力(如 useStateuseContextuse)和并发渲染等基础能力,而将状态管理方案的选择权完全交给开发者和社区,这是一种“授人以渔”而非“授人以鱼”的理念,与Vue 官方相比较的话,Vue 官方理念更偏向于框架应当努力降低开发者的心智负担,让开发者更专注于业务。这二者理念没有孰强孰弱之分,只能说二者皆过犹不及。

React 19.x 版本推出的React 编译器(React Forget)自动代理了useMemo/useCallback 优化,这也能侧面说明React 官方也认为以往只专注于底层核心职责有些极端了,应当适当改变.

本文参考