背景
目前老项目使用的 redux 性能较差且难上手/代码复杂,useContext 虽然使用上简单但性能较差,需要选型一个易上手/性能好/组件化的三方库,以下是 redux/useContext 在处理复杂业务时的缺点
- 组件间的状态共享只能通过将 state 提升至它们的公共祖先来实现,但这样做可能导致重新渲染一颗巨大的组件树(管理成本和子组件意外渲染问题)
- Context 只能存储单一值,无法存储多个各自拥有消费者的值的集合
- 以上两种方式都很难将组件树的顶层与子组件进行代码分割
- useContext 也没有提供对异步请求的解决方案,redux 虽然有但较难上手
目标
提供一种机制来管理和维护 React 应用中的状态,并且使得这些状态能够跨组件共享、状态的变化可以预测
- 做到数据共享,兼顾子组件精准渲染
- 获取和修改状态
- 管理异步工作流
模式
Store
Store 模式通过集中式的存储来管理应用的状态,这种模式的基本思想是将应用的所有状态存储在一个单一的地方,通常称为“store”。组件通过与 store 的交互来读取和更新状态,而不是直接在组件内部管理状态
Redux、Zustand 正是使用了 store 模式设计,Redux 官网的 gif 很好的演示了 redux 中数据流动的方向及 Store、 Dispach、Action、Reducer 的配合过程

响应
响应模式是一种通过观察(observe)状态变化并自动更新界面的方法。这种模式的核心思想是,当数据发生变化时,界面能够自动地响应并更新显示,可以使得状态管理更加直观、高效和模块化
MobX、Valtio 都使用了响应模式,MobX 官网流程图很好的解释了其 data flow
原子
不同于 Store 模式和响应模式把 State 集中起来管理,状态维护在组件顶部,子组件需要的话通过 selector 按需获取,数据自上向下流动。原子模式提倡把应用的全局状态拆分为多个小的、独立的状态单元——这些状态单元被称为原子(Atom),提供更细粒度的状态管理,以便组件可以更高效地更新和渲染
原子模式是对 useState + useContext 的升级,原子模式的代表作是 Recoil 和 Jotai
状态机
状态机是一种数学模型,它描述了在任何给定时间只能处于一种状态的系统,系统在不同的状态之间可以通过确定的事件进行转化
选型
| 状态库 | 响应粒度 | API 简洁度 | 性能 | 类型支持 | 异步处理 | 持久化/中间件 | DevTools | 社区生态 | 应用规模 | 适用场景/特点 | 适用具体范围 | 优点 | 缺点 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Context API | 粗粒度 | 简单 | 一般 | 完善 | 需手动 | 需手动 | 无 | 极成熟 | 小/中 | 全局共享、简单场景,易用 |
|
|
|
| Redux | 粗粒度 | 复杂 | 一般 | 完善 | 完善 | 完善 | 强(Redux DevTools) | 极成熟 | 中/大 | 可预测性、可扩展性、生态强 |
|
|
|
| MobX | 中等粒度 | 简单 | 高 | 完善 | 支持 | 支持 | 有 | 成熟 | 中/大 | 响应式、面向对象、灵活 |
|
|
|
| Recoil | 原子粒度 | 适中 | 高 | 完善 | 完善 | 需手动 | 有(Recoil DevTools) | 较成熟 | 中/大 | 原子+选择器、复杂依赖关系 |
|
|
|
| Zustand | 任意粒度 | 极简 | 高 | 完善 | 需手动 | 完善 | 有(Zustand DevTools) | 极成熟 | 小/中/大 | Hook 风格、中间件、极简 |
|
|
|
| Jotai | 原子粒度 | 极简 | 高 | 完善 | 支持 | 需手动 | 有(Jotai DevTools) | 新兴 | 小/中 | 函数式、原子化、极简 |
|
|
|
| XState | 状态机/流程 | 复杂 | 高 | 完善 | 完善 | 支持 | 有 | 成熟 | 中/大 | 复杂业务流程、可视化 | 多状态流转、流程编排、复杂异步与副作用管理 |
|
|
| Preact Signals | 极细粒度 | 极简 | 极高 | 好 | 需手动 | 需手动 | 有限 | 新兴 | 小/中 | 微秒级响应、性能极致 |
|
|
|
| Pinia | 原子/模块粒度 | 简单 | 高 | 完善 | 支持 | 支持 | 有 | 成熟 | 小/中/大 | Vue 原生、社区有 React 适配 |
|
|
|
| helux | 极细粒度 | 丰富 | 极高 | 极好 | 完善 | 有限/需手动 | 有 | 新兴 | 小/中 | 响应式+按需渲染、类型友好 | 需要极细粒度渲染优化与强类型结合场景 |
|
|
对比
代码
Context API
import React, { createContext, useContext, useState } from 'react';
const DemoContext = createContext();
const DemoProvider = ({ children }) => {
const [count, setCount] = useState(0);
const [name, setName] = useState('Anonymous');
return <DemoContext.Provider value={{ count, setCount, name, setName }}>{children}</DemoContext.Provider>;
};
const GrandSonName = () => {
const { name } = useContext(DemoContext);
return (
<div style={{ border: '1px #333 solid' }}>
<h3>GrandSonName</h3>
<p>name: {name}</p>
</div>
);
};
const GrandSonCount = () => {
const { count } = useContext(DemoContext);
return (
<div style={{ border: '1px #333 solid' }}>
<h3>GrandSonCount</h3>
<p>count: {count}</p>
</div>
);
};
const Son1 = () => {
const { count, setCount, name } = useContext(DemoContext);
return (
<div style={{ border: '1px #333 solid', padding: 5 }}>
<h2>Child 1</h2>
<p>Count: {count}</p>
<p>Name: {name}</p>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<GrandSonName />
<GrandSonCount />
</div>
);
};
const Son2 = () => {
const { name, setName } = useContext(DemoContext);
return (
<div style={{ border: '1px #333 solid', padding: 5, marginTop: 10 }}>
<h2>Child 2</h2>
<input type='text' value={name} onChange={(e) => setName(e.target.value)} />
<GrandSonName />
</div>
);
};
const Son3 = () => {
return (
<div style={{ border: '1px #333 solid', padding: 5, marginTop: 10 }}>
<h2>Child 3</h2>
<GrandSonName />
<GrandSonCount />
</div>
);
};
export default function App() {
return (
<DemoProvider>
<Son1 />
<Son2 />
<Son3 />
</DemoProvider>
);
}helux
import { sharex, $, useAtom } from 'helux';
import React from 'react';
const GlobalModel = sharex({
count: 1,
name: 'Anonymous'
});
const { state, setState } = GlobalModel;
const GrandSonName = () => {
return (
<div style={{ border: '1px #333 solid' }}>
<h3>GrandSonName</h3>
<p>name: {$(state.name)}</p>
</div>
);
};
const GrandSonCount = () => {
return (
<div style={{ border: '1px #333 solid' }}>
<h3>GrandSonCount</h3>
<p>count: {$(state.count)}</p>
</div>
);
};
const Son1 = () => {
return (
<div style={{ border: '1px #333 solid', padding: 5 }}>
<h2>Child 1</h2>
<p>Count: {$(state.count)}</p>
<p>Name: {$(state.name)}</p>
<button
onClick={() =>
setState((draft) => {
draft.count += 1;
})
}
>
Increment Count
</button>
<GrandSonName />
<GrandSonCount />
</div>
);
};
// 这里需要抽出一个组件,否则依然会有性能问题
const Input = () => {
const [dictState] = useAtom(state);
return (
<input
type='text'
value={dictState.name}
onChange={(e) => {
setState((draft) => {
draft.name = e.target.value;
});
}}
/>
);
};
const Son2 = () => {
return (
<div style={{ border: '1px #333 solid', padding: 5, marginTop: 10 }}>
<h2>Child 2</h2>
<Input />
<GrandSonName />
</div>
);
};
const Son3 = () => {
return (
<div style={{ border: '1px #333 solid', padding: 5, marginTop: 10 }}>
<h2>Child 3</h2>
<GrandSonName />
<GrandSonCount />
</div>
);
};
export default function App() {
return (
<>
<Son1 />
<Son2 />
<Son3 />
</>
);
}效果
| 状态库 | React 层重渲染 | 浏览器重渲染 |
|---|---|---|
| Context API | ||
| helux |
结论
以 React 层重渲染(即组件是否触发 render)为准,此标准比浏览器重渲染更高,由上面的视频可得出以下结论
-
虽然最终浏览器重渲染的效果一样,但 helux 对比 Context API 明显在 React 层性能更好,归功于 helux 的 dom 粒度更新
-
helux 的 signal 语法只适用于展示型即普通的 dom 元素,无法适用于 react 组件,需要使用 useAtom 转换为真实状态,但此时就失去了 dom 粒度更新的优势,需要将 react 组件抽出一个单独组件防止性能劣化
快速上手
https://heluxjs.github.io/helux/
全局状态
文件夹结构
├── model
│ ├── actions.js
│ ├── index.js
│ └── state.jsindex.js
导出主入口
import { $, useAtom } from 'helux';
import { GlobalState } from './state';
import * as actions from './actions'; // action 函数定义
export default GlobalState;
export const action = GlobalState.defineActions()(actions);
const { state, setState } = GlobalState;
export { $, useAtom, state, setState };state.js
全局状态定义
import { sharex } from 'helux';
export const GlobalState = sharex(
{
key: null,
key2: null,
jobInfo: {}
},
{ moduleName: 'JobDetailState' }
);actions.js
异步 action 定义
import { getJobInfo } from '../services';
export async function queryJobInfo({ draft, payload }) {
const [error, res] = await getJobInfo({ id: payload.jobId });
if (error) {
return null;
}
draft.jobInfo = res;
return res;
}纯展示类型
import { $, state, setState } from './model';
// 纯展示型即 dom 结点直接使用 signal 语法,即 $(state.key)
export default function Example() {
return (
<div>
<Button
onClick={() =>
setState((draft) => {
draft.key += 1;
})
}
>
{$(state.key)}
</Button>
</div>
);
}React 状态类型
import { $, state, setState, useAtom } from './model';
// 这里需要抽出一个组件隔离渲染,即 useAtom 会触发组件重渲染
const Input = () => {
const [dictState] = useAtom(state); // 由于下面使用了 key2,更新 key2 时才会触发重渲染,即更新 key 时不会触发重渲染
return (
<input
type='text'
value={dictState.key2}
onChange={(e) => {
setState((draft) => {
draft.key2 = e.target.value;
});
}}
/>
);
};
// React 组件由于需要取到原始值,需要使用 useAtom 包一下取出
export default function Example2() {
return (
<div>
<Input />
{$(state.key2)}
</div>
);
}异步请求类型
这里将请求放到全局状态里面,而不是组件里面,推荐在全局状态和异步请求有关时,优先在这里去请求,理由如下
-
为了避免组件的重渲染,即如果请求在顶级组件的话,那么所有组件都会重渲染
-
写成 action 也有利于调试,安装 Redux DevTools 插件
import { $, state, setState, action } from './model';
const JobDetail = ({ jobId }) => {
// const { queryJobInfo } = action.useLoading();
// console.log('useLoading', queryJobInfo) // 请求的状态
const handleEvent = () => {
console.log('state.jobId', state.jobInfo); // 事件触发的可以直接通过 state 拿到最新值
};
return (
<div className={styles.container}>
<Button
onClick={() =>
setState(async (draft) => {
const data = await action.eActions.queryJobInfo({ jobId });
console.log('data', data);
})
}
>
{$(state.failedFNGPMetrics.total)} // 用 $ 或者 useAtom 取到
</Button>
</div>
);
};
export default JobDetail;