文章结构:
- 状态管理的概念
- 状态管理工具的分类与比较
- 如何选择状态管理工具
- 思维导图做个总结
本文不会介绍各个状态管理工具的具体使用或者如何二次封装,而是从状态管理的概念入手,讲解我们应该关注状态管理工具的什么、常用状态管理工具的分类和比较、以及实际业务中如何去选择状态管理工具,让你对状态管理有更进一步的理解。
状态管理的概念
状态,是指数据的变化,而状态管理,也就是维护应用的数据变化。
它可以是视图状态,比如一个弹窗的visible、Tab的选中状态、激活的路由、是否有loading等;
也可以是逻辑状态,比如异步请求后端返回的数据(用户信息、列表内容等),或者是某个功能里各个状态之间的关系(比如文件上传这一功能中的上传进度、上传信息、是否断网、是否重连等)。
在react中,你可以使用useState或者this.state把状态维护在组件内部,通过props传递给子组件去使用。
如果是兄弟组件想要共享状态,也可以把状态抽离至公共父组件,然后同样通过props形式传递给子组件使用。
当两个组件离得比较远,但是又想共享状态,那么你可能就需要考虑用其他的方式来维护这些状态。这也就是引入状态管理要解决的一个问题:如何在组件间优雅的共享一些状态。
还有一种情况:当A组件的某个状态发生变化时,B组件的某个状态也需要跟着变化,可能是同步的变化,也可能是异步的变化。这是引入状态管理需要解决的另一个问题:如何 把各项状态之间的逻辑与其他系统模块之间的互动逻辑进行组织。
针对第二种情况,我们举一个典型的例子:
在逛某个App时,突然来了几条内部应用的通知,但是去消息页面查看时,却看不到那几条消息,需要手动刷新一下才能看见。这可能就是因为这个App的内部状态管理对于这两个状态的互动逻辑有所欠缺导致。
(我觉得掘金web端消息提醒也有类似的问题,被点赞后进去点赞消息页,提示的小红点并没消失,需要再点其他tab才消失,不知道是不是产品就想要这种效果)
状态管理工具的分类与比较
实现状态管理的方式:
- Context API
- redux
- zustand
- mobx
- recoil
- jotai
- xState
- ...
状态管理方式特别多,我们关注的点可以聚焦在以下三个点:
- 状态管理器如何获取和设置基本的状态值,因为这是我们使用状态管理时最常做的两件事
- 状态管理器如何管理异步工作流,因为应用中可能做了很多异步工作
- 状态管理器是如何处理数据之间的联动(派生状态)
除此之外,我们还可以通过以下维度进行对比:
- 压缩前后的大小
- GitHub上的star数/npm每周下载量
- 社区活跃度(以Stack Overflow 上带标签的问题数为例)
这些属于数据类的比较,我们通过一个表格直观的来展示:
分类
在单独分析某一个状态管理工具之前,我们先根据其设计理念做一个分类:
- 单向数据流:redux、zustand
- 响应式:mobx
- 状态原子化:recoil、jotai
下面就挑Context、redux、mobx、recoil这几种状态管理方式讲一讲。
Context API
Context API并不是一个状态管理工具,他是React内置的状态管理功能。使用Context 的useContext+useReducer是基本可以实现redux的功能的,所以也是一种可行的状态管理方式。
如何获取和设置基本的状态值
Context API 用 useContext 生成一个useXxxContent,调用useXxxContent可以从其返回的对象拿到state和一个dispatch,你可以用这个dispatch来修改状态,也可以对dispatch做一个封装,实现一个可以修改state的函数。
const { state, dispatch } = useContext(StateContext);
// ...
dispatch({
type: CHANGE_INPUT,
inputValue: e.target.value,
});
复制代码
如何处理异步
对于异步的逻辑,Context API并没有提供任何API,需要自己做封装。
如何处理数据间的联动
Context API并没有提供API来生成派生状态,同样也需要自行去封装一些方法来实现。
优点
- 作为React内置的hook,不需要引入第三方库
- 书写还算方便
缺点
- Context 只能存储单一值,当数据量大起来时,你可能需要使用createContext创建大量context。
- 直接使用的话,会有一定的性能问题:每一次对state的某个值变更,都会导致其他使用该state的组件re-render,即使没有使用该值。 你可以通过useMemo来解决这个问题,但是就需要一定的成本来定制一个通用的解决方案。
redux
redux是GitHub star数和周下载量都最多的状态管理工具。他的工作流程大致如下:
- 用户在view层触发某个事件,通过dispatch发送了action和payload
- action和payload被传入reducer函数,返回一个新的state
- store拿到reducer返回的state并做更新,同时通知view层进行re-render
从上面的流程图我们可以看出,redux设计的思路就是单向数据流。
除此之外,redux还遵循了以下原则:
- 单一数据源。 redux的store只有一个,所有的状态都放在store中,所有的state共同组成了一个树形结构。
- state是不可变的。 在redux中修改state的方式是dispatch一个action,根据action的payload返回一个新的state。
- 纯函数修改。 redux通过reducer函数来修改状态,它接受前一次的state和action,返回新的state,只要传入相同的state和action,一定会返回相同的结果。
也就是这三个原则让redux的状态是可预测的。
如何获取和设置基本状态值
react-redux提供了两个API:useSelector、useDispatch来获取和设置状态,在函数式组件中设置/获取要比在class组件更方便。
如何处理异步
redux没有规定如何处理异步数据流,最原始的方式就是使用Action Creators,也就是在制造action之前进行各种的异步操作,你可以把要复用的操作抽离出来。
当然这样并不优雅,在实际项目中我们通常使用类似redux-thunk、redux-saga这些中间件来支持处理异步。
如何处理数据间联动
react-redux的useSelector获取状态后,你可以编写一些逻辑来处理派生状态。如果派生状态需要复用,记得给抽离出来。
优点
- 繁荣的社区,像不支持异步这种问题是由成熟的中间件可以解决的,你遇到的问题多多少少可以在社区找到答案。
- 可扩展性高,中间件模式让你可以随心所欲的武装你的dispatch。
- 单一数据源且是树形结构,这让redux支持回溯,在调试上也更方便。
缺点
- 大量的模版代码,写起来挺累人的,使用redux toolkit可以一定程度的减少。
- 状态量大起来后,有可能会出现性能问题。要是啥玩意都往redux里存,可想而知,每次action过来把所有reducer跑一遍,多少有点噩梦的。当然redux后面开始支持拆分store,异步去加载store,没到这个业务的场景的时候不加载这个业务的store。但是如果业务耦合较为严重,那还是跑不掉。
mobx
mobx是一个非常典型的响应式状态管理工具。他的工作流程大致如下:
- 用户在view层触发某个事件
- 事件触发action执行,通过action来修改state
- state更新后,computed Values也会根据依赖重新计算属性值
- 状态更新后会触发reactions,来响应这次状态变化的一些操作(重新渲染组件、打印日志...)
mobx这种响应式的设计和vue很类似。与redux不同,mobx对全局state做了一层代理,监听state的变化,当state变化时,会自动更新相关的计算属性,所以mobx修改state是直接修改。
如何获取和设置基本的状态值
写完store后直接引入就可以:
const { xxxState, setXxxState } = xxxStore;
复制代码
如何处理异步
mobx中的Actions指的是一段可以改变state的代码,他没有任何限制,对于修改state前的异步处理,放到Actions中即可,所以mobx对于异步的处理是很自然的。
如何处理数据间的联动
mobx提供了计算属性来处理派生状态,和vue的computed value很类似,基于state使用纯函数计算出另一个值。
优点
- 上手简单。没有太多的概念和API,你只需要在store里声明你的state和修改state的方法就可以用了。
缺点
- 风格自由。如果没有统一团队的代码风格,那你可能会在store中看到各种各样的代码。
Recoil
Recoil是React官方推出的一个状态管理库,设计的思路是将状态原子化。atom和selector是Recoil的两个核心。
atom:一个原子是一个共享状态的片段。
selector:一个组件可以订阅一个原子来获取/设置它的值。
网上的一张图很贴切:
每一个Atom是一个可订阅可修改的state单元。
如何获取和设置基本的状态值
要消费一个状态的时候,需要import两个东西:
import { useRecoilState } from "recoil";
import { xxxState } from "../store";
useRecoilState(xxxState);
复制代码
如何处理异步
Recoil提供了一个useRecoilValueLoadable来处理异步操作。
首先:
const userInfo = selector({
key: "userInfo",
get: async () => {
const res = await getUserInfo();
return res.name;
}
});
复制代码
然后用useRecoilValueLoadable来消费:
const Info = () => {
const userInfoLoadable = useRecoilValueLoadable(userInfo);
switch (userInfoLoadable.state) {
case "hasValue":
// ...
case "loading":
// ...
case "hasError":
// ...
}
};
复制代码
如何处理数据间的联动
Recoil提供了selector来处理派生状态。比如我们的按钮是否可见跟随着tab的变化而变化:
import { CurrentTab } from './atom';
export const buttonVisible = selector({
key: 'buttonVisible',
get: ({ get }) => {
const tab = get(CurrentTab);
const buttonVisible = tab === 'A' ? true : false;
return buttonVisible;
},
});
复制代码
优点
- React官方推出的状态管理工具,有保障性。
- 对 React concurrent 模式支持良好。
缺点
- 不支持class组件。如果你是要更换项目的状态管理工具,应该看看原本项目中有没有用到class组件。
- API比较多,上手稍微有点成本。
- 消费一个状态比较繁琐,需要import两个东西。
如何选择状态管理工具
在选用或者更换状态管理工具之前,你可以询问自己:目前的状态管理方式,对于我的应用来说足够了吗?如果是更换状态管理工具,那么能解决什么问题,带来什么好处?
没有最好的状态管理方式,只有合适的状态管理方式。
根据上一小节对各个状态管理的比较,我们可以根据他们的优缺点来判断我们实际业务中到底选用哪一种方式。
- 如果你的应用中,每个页面里各个部分的功能都比较独立,没有什么联动,那么你想要共享状态可能组件跨度并不大,或许就不需要状态管理器,提升状态至父组件然后使用props传递给子组件是一个选择。
- 如果组件跨度较远,但是应用的需要共享的状态不多,想要共享的状态更新频率也不高,可以使用Context API解决。
- 当然,从前面我们对context的分析可以看出,对于异步、派生状态、如何控制re-render等,我们都需要自己去封装一些通用的方法,所以当共享状态偏多,更新频率不低,我们就可以考虑使用状态管理工具来接管。
- redux、mobx、recoil属于各有所长吧,无法直接说哪个更好,我做了些的归纳:
-
- 如果组里有对于redux的一些封装和规范,那么redux会是一个好选择
- 如果业务需要频繁更新状态(如在移动某个div时需要实时共享坐标),redux或许不太合适,mobx和recoil更为合适
- 如果要共享的状态不算多,那么使用redux或许不太合适,mobx使用起来更为简单,recoil也可以试试
- 如果组里有对于mobx的一些封装和规范,那么mobx会是一个好选择
当然还有一点很重要的,就是组里的人想用,用的顺手哈,毕竟状态管理工具是工具,用工具的是人,哪样顺手使用者说了算。
总结
- 状态管理工具并不是必需品,有时候context API就够了。
- 不要什么都存到状态管理工具里头去 !!!想清楚到底需不需要共享,有时候就是抽离到父组件的事。
- 对于中大型项目,更重要的是制定代码规范,最好有相关的code review机制,要不然一段时间后你的状态管理就会像一团拉面一样(深有体会)
最后我们用一个思维导图来总结下本文的内容: