状态管理
现代前端框架把对 "过程" 的各种命令,变为了对 "状态" 的描述。因此前端开发也就从组合各式各样的命令完成用户界面更新和交互,变成了以状态驱动 web 界面变化的状态机式开发方式。
Redux

核心原理
- 单一数据源 :整个应用状态存储在一棵对象树中,这棵树只存在于单个 store 中。优点是状态更加可预测,方便监控和调试。
- 状态只读:唯一改变状态的方法是触发一个 Action。这样做确保状态变更是可以被跟踪和集中处理,同时还避免了不同部分的代码改变状态时产生冲突。
- 纯函数修改状态:改变状态的逻辑是 reducer 决定的。这让行为可预测,有利于测试和服务端渲染。
Redux 的优势
- 数据流清晰,改变数据有统一的入口。
组件里都是通过 dispatch 一个 action 来触发 store 的修改,而且修改的逻辑都是在 reducer 里面,组件再监听 store 的数据变化,从中取出最新的数据。这样数据流动是单向的,清晰的,很容易管理。

- 异步过程的管理灵活
用 redux-saga 或 redux-observable 中间件,解决了组件里的异步过程不好复用、多个异步过程之间不好做并行、串行等控制的复杂异步过程处理问题,可根据场景的复杂度灵活选用。
Redux 的问题
- 学习曲线高。Redux 中有很多概念需要学习,比如 actions、reducers、store、middleware 等。
- 样板代码多。一点小的改动,可能需要在多个文件中进行对应的修改。
- 性能一般 。对大型项目,每次更新都需要通过中央的 reducer,粒度太粗。这可能会成为性能瓶颈。
- 对 Hooks 支持一般。Redux 出现的时间很早,比 Hooks 更早。后面是依靠 RTK 来对 Hooks 进行支持的。
- 单向数据流。并不是所有人都认为单向数据流是最好的做法,基于代理的方式同样可以做这件事。
- 框架生态的多样化。
Umi:Dva
Demo:
js
export default {
namespace: "counter", // 模型的命名空间,区分多个模型
state: {
count: 0 // 初始状态,计数器的值
},
reducers: {
// Reducers 处理同步操作,用于修改状态
add(state, { payload }) {
return { ...state, count: state.count + payload }; // 增加计数
},
minus(state, { payload }) {
return { ...state, count: state.count - payload }; // 减少计数
}
},
effects: {
// Effects 处理异步操作和业务逻辑,还可以触发其他 action
// `put` 用于触发 action,`call` 用于调用异步过程,`select` 用于从 state 里获取数据
*asyncAdd({ payload }, { put, call }) {
// 模拟异步操作
yield call(delay, 1000); // 延迟1s
yield put({ type: "add", payload }); // 调用 reducer 来增加计数
},
*asyncMinus({ payload }, { put, call }) {
// 模拟异步操作
yield call(delay, 1000); // 延迟1s
yield put({ type: "minus", payload }); // 调用 reducer 来减少计数
}
},
subscriptions: {
// Subscriptions 用于订阅数据源,并根据需要分发相应的 action
// 这里就不包含具体的实现,仅作展示用
setup({ dispatch, history }) {
// 监听 history 变化,当进入 `/` 时触发 `asyncAdd` effect
history.listen(({ pathname }) => {
if (pathname === "/") {
dispatch({ type: "asyncAdd", payload: 1 });
}
});
}
}
};
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
核心原理
- Dva 基于现有框架集成:把 Redux、Redux-Saga、React-router 等框架都集成到一起,可以很方便地进行开发。
- 做出了一些创新,提出了 Model 的概念,将 state、reducers、effects 和 subscriptions 都集中到一起,这样组织代码会更加模块化和简洁。
Dva 的问题
- 继承了 Redux 的高学习曲线。对新手不太友好,而精通 Redux 的开发者又未必会选择 Dva。
- 依赖过多:dva.js 依赖于多个库,意味着 dva.js 的项目会有更多的依赖,这可能会导致项目的维护和升级变得更加复杂。
- 过度封装。React 本身的特性一直在持续演进,像 Redux 和 Dva 这类独立于 React 之外的数据管理方案就显得没那么完美契合。同时 Dva 的封装也可能让一些 Redux 生态中优秀的工具难以整合到框架中。
- 更新较慢 :dva.js 的更新速度相比其他框架来说较慢,不能及时跟上最新的技术发展。dva 仓库在 2019 年开始就不再维护了 .
Zustand
Zustand 借鉴了一些 Redux 中的思路,但在具体的设计上截然不同。
基本使用
js
import React from "react";
import create from "zustand";
// 使用 zustand 创建一个 store,它会保存状态并提供修改这些状态的方法
const useCounterStore = create((set) => ({
count: 0, // 初始状态
// 定义修改状态的方法
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}));
const Counter = () => {
// 使用自定义的useCounterStore钩子来订阅状态变化
// select参数是一个函数,用来选择性订阅state中的特定部分
const { count, increment, decrement, reset } = useCounterStore();
return (
<div>
<h1>计数器: {count}</h1>
<button onClick={increment}>增加</button> {/* 触发增加计数 */}
<button onClick={decrement}>减少</button> {/* 触发减少计数 */}
<button onClick={reset}>重置</button> {/* 触发重置计数 */}
</div>
);
};
export default Counter;
设计原理
zustand = 发布订阅 + react hooks
zustand 的心智模型非常简单,包含一个发布订阅器和渲染层,工作原理如下,

- Vanilla 层:发布订阅模式的实现,提供了setState、subscribe 和 getState 方法,并且前两者提供了 selector 和 equalityFn 参数,以供在只有原生 JS 模式下的正常使用。
- React 层:Zustand 的核心,实现了 reselect 缓存和注册事件的 listener 的功能,并且通过 forceUpdate 对组件进行重渲染。
Zustand 的优势
- 简洁的 API:创建 store 和访问 state 都非常直观。
- React Hooks:Zustand 与 React hooks 完美整合,使得在 React 组件中使用状态变得非常简单。
- 没有"单一真相来源"的限制:允许创建多个独立的 store,给予开发者更多的灵活性。
- 简化状态管理:提供一个更直观的 API,状态管理更加简洁和直接。
- 简单的状态共享 :不需要使用 context providers 包裹应用、也没有 reducer 那种模版代码,状态可以跨组件和文件轻松共享。状态和逻辑实现了高内聚。
- 由于没有 Provider 的存在,所以声明的 useStore 默认都是单实例,如果需要多实例的话,zustand 也提供了对应的 Provider 的书写方式。
- 中间件和增强功能:支持中间件,使得开发者可以轻松添加日志记录、持久化存储等增强功能。
- 适应现代 React 功能:考虑到了 React 的新特性,如 Concurrent Mode 和 Suspense,从而确保在现代 React 特性下的稳定性。
- 性能优化:Zustand 允许组件仅订阅状态的一部分,减少不必要的渲染和提高性能。
Zustand 的问题
- Zombie Child Problem (僵尸子组件问题)
在 React 的异步渲染环境中,当一个 React 组件的状态更新后,子组件可能会在一个渲染周期中引用旧的父组件状态。这意味着子组件表现得好像它们"僵死"在了一个过时的状态上,与当前的应用程序状态不同步。
- React Concurrency (React 并发问题)
React 16.8 引入了 Concurrent Mode,它带来了新的并发功能,可以让 React 在渲染过程中中断和恢复工作。这种能力提高了应用的响应性和性能,但同时也引入了复杂性,因为开发者需要确保他们的状态管理能够适应可能出现的中断和重新开始的渲染。
- Context Loss Between Mixed Renderers (混合渲染器之间的上下文丢失问题)
当在同一个应用程序中混合使用不同类型的渲染器时,可能会遇到上下文丢失的问题。这是因为 React 的上下文机制是按渲染器实例进行隔离的。如果状态管理库不正确地处理这些情况,可能会导致跨不同渲染器的组件状态不一致。
Zustand vs Redux
特性 | Zustand | Redux |
---|---|---|
状态模型 | 不可变状态 | 不可变状态 |
Context | 不需要 | 需要使用 Provider |
API | 简洁 | 标准 Redux 需要 action、reducer;Redux Toolkit 提供简化的 API |
代码样板 | 较少 | 较多(尽管 Redux Toolkit 有所简化) |
渲染优化 | 手动使用选择器 | 手动使用选择器(Redux Toolkit 中 selector 的使用更为普遍) |
状态更新 | 直接通过 store 函数 | 通过 dispatch 和 reducer |
中间件支持 | 有支持 | 有支持,中间件生态丰富 |
其他 | 无需包装应用,易于集成 | 广泛的社区和生态系统支持,适合大型应用 |
Jotai
Jotai 是一个小型全局状态管理库,是 Context 和订阅机制的结合,它模仿了 useState、useReducer。
场景:如果是简单的两个组件之间的简单的数据共享,jotai可能是合适的工具
demo:jotai 编写计数器
js
import React from 'react';
import { atom, useAtom } from 'jotai';
// 创建一个 Jotai 原子代表计数器的状态
// 这个原子可以在组件间共享
const counterAtom = atom(0); // 参数 0 是原子的初始值
// 创建对应的 setter 原子,用于修改 counterAtom 的值
// 这种方式可以实现更复杂的状态逻辑和更好的模块化
const incrementAtom = atom(
(get) => get(counterAtom), // 获取当前的计数器值
(get, set) => set(counterAtom, get(counterAtom) + 1) // 实现增加操作
);
const decrementAtom = atom(
(get) => get(counterAtom),
(get, set) => set(counterAtom, get(counterAtom) - 1) // 实现减少操作
);
const resetAtom = atom(
(get) => get(counterAtom),
(get, set) => set(counterAtom, 0) // 实现重置操作
);
// 计数器组件
const Counter = () => {
// 使用 useAtom Hook 绑定到特定的原子状态和操作
const [count, ] = useAtom(counterAtom);
const [, increment] = useAtom(incrementAtom);
const [, decrement] = useAtom(decrementAtom);
const [, reset] = useAtom(resetAtom);
return (
<div>
<h1>计数器: {count}</h1>
<button onClick={increment}>增加</button> {/* 调用增加操作 */}
<button onClick={decrement}>减少</button> {/* 调用减少操作 */}
<button onClick={reset}>重置</button> {/* 调用重置操作 */}
</div>
);
};
export default Counter;
功能和特点
- 原子:Jotai 的核心概念是原子,它代表了应用程序中的一个独立的状态单元。
- 响应性:Jotai 使用了 React 的上下文(Context)和钩子(Hooks)机制,实现了高效的响应性。当原子的状态发生变化时,相关的组件将自动重新渲染,确保应用程序保持同步。
- 状态组合:Jotai 允许开发人员将多个原子组合成一个更大的状态单元。
- 优雅的 API:Jotai 提供了一组简洁而直观的 API,使开发人员能够轻松地定义和使用原子,避免冗长的状态管理代码和繁琐的生命周期方法。
Jotai 的优势
- 简化的状态管理
- 响应式和高性能
- 轻量级和灵活性(体积小,2kb)
- 社区支持和生态系统
Jotai 的缺陷
- 处理异步时很麻烦
- 无法在非 react 上下文中使用
Jotai VS Zustand
特性 | Zustand | Jotai |
---|---|---|
状态模型 | 不可变状态 | 原子状态(可组合的小单位状态) |
Context | 不需要 | 不需要 |
API | 直观、简洁 | 细粒度、可组合 |
代码样板 | 较少 | 较少 |
渲染优化 | 手动使用选择器 | 原子依赖自动优化 |
状态更新 | 直接通过 store 函数 | 使用原子更新和订阅状态 |
中间件支持 | 有支持 | 不适用(原子模型提供不同的扩展方式) |
其他 | 易于集成 | 原子模型提供更高的可组合性,适用于需要细粒度状态管理的场景 |
什么时候用哪个
- 如果需要替换 useState + useContext,Jotai 很合适,即原子化的组件状态管理或少量组件间状态共享。
- 如果想在 React 之外更新状态,Zustand 效果更好。
- 如果代码拆分很重要,Jotai 应该表现良好。
- 如果想使用 Suspense,Jotai 就是其中之一。
MobX
核心原理
围绕着响应式编程和观察者模式进行构建,通过类似响应式编程的方式来管理数据流。
通过使用可观察的值(Observable),自动追踪状态变化,然后通过反应机制(Reaction)自动更新视图,减少了手动维护状态和视图同步需要的样板代码,降低了状态管理的复杂性
js
import React from "react";
import { observable, action, makeAutoObservable } from "mobx";
import { observer } from "mobx-react";
// 创建一个计数器的 store
class CounterStore {
// 使用 observable 装饰器标记 count 为可观察的状态
count = 0;
constructor() {
// 使用 makeAutoObservable 包装这个类,让 count 成为响应式状态
// 并让类中的方法拥有正确的 action 绑定
makeAutoObservable(this);
}
// 使用 action 装饰器标记 increment 为一个动作,用于修改状态
increment = () => {
this.count += 1;
};
// 使用 action 装饰器标记 decrement 为一个动作,用于修改状态
decrement = () => {
this.count -= 1;
};
// 使用 action 装饰器标记 reset 为一个动作,用于重置状态
reset = () => {
this.count = 0;
};
}
// 创建 store 实例
const counterStore = new CounterStore();
// 创建一个观察者组件,它被 observer 高阶组件包装以响应状态变化
const Counter = observer(() => {
return (
<div>
<h1>计数器: {counterStore.count}</h1>
{/* 当按钮被点击时,调用 store 中对应的动作函数修改状态 */}
<button onClick={counterStore.increment}>增加</button>
<button onClick={counterStore.decrement}>减少</button>
<button onClick={counterStore.reset}>重置</button>
</div>
);
});
// 渲染计数器组件
const App = () => {
return (
<div>
<Counter />
</div>
);
};
export default App;
MobX 的问题
- 学习曲线高,响应式编程和单向数据流相比同样有一些概念需要理解。
- 和单向数据流相比,MobX 数据的流动性并不会那么明确和显式。
Hox
在 Dva 出现 3 年后,Dva 的作者创建了一个新的项目:umi。umi 是一个现代 React 企业级框架,其中状态管理方案就是 Hox。
Hox 是以一种非常接近于原生 React 的方式来组织代码的。定义 Model 的 Hook,实际上就是创建一个函数,然后在函数中使用原生 API useState,可谓是极其精简和干净。
js
import { useState } from 'react';
import { createModel } from 'hox';
// 创建一个自定义 Hook,用于实现计数逻辑
function useCounter() {
// 使用 useState 初始化计数器的 state
const [count, setCount] = useState(0);
// 创建增加计数的方法
const increment = () => {
setCount((prevCount) => prevCount + 1);
};
// 创建减少计数的方法
const decrement = () => {
setCount((prevCount) => prevCount - 1);
};
// 创建重置计数器的方法
const reset = () => {
setCount(0);
};
// 返回 count 状态以及修改状态的方法
return {
count,
increment,
decrement,
reset,
};
}
// 使用 createModel 将上面的自定义 Hook 转换成一个可以在组件间共享状态的 Model
export default createModel(useCounter);
// UI 组件中使用
import React from 'react';
// 引入我们建立的 Counter Model
import useCounterModel from './useCounterModel';
function Counter() {
// 从我们的 Model 中获得状态和操作方法
const { count, increment, decrement, reset } = useCounterModel();
return (
<div>
<h2>计数器: {count}</h2>
<button onClick={increment}>增加</button> {/* 点击按钮增加计数 */}
<button onClick={decrement}>减少</button> {/* 点击按钮减少计数 */}
<button onClick={reset}>重置</button> {/* 点击按钮重置计数 */}
</div>
);
}
export default Counter;
相关建议
- 如果是小型项目且没有多少状态需要共享,那么不需要状态管理,react props 或者 context 就能实现需求。
- 如果需要手动控制状态的更新,单向数据流是合适的选择,例如:redux,zustand
- 如果需要简单的自动更新,双向绑定的状态管理是不二之选,例如:mobx,valtio
- 如果是两个或多个组件之间简单的数据共享,那么原子化或许是合适的选择,例如:jotai,recoil
- 如果有在非 react 上下文订阅、操作状态的需求,那么 jotai、recoil 等工具不是好的选择。