最近梳理了团队内的前端项目,发现React的状态管理工具用的很杂,有dva、rematch、mobx、hooks、redux等,如此分裂的工具会导致日常的工作效率极低,因为每个人都需要掌握多个工具库的使用,再加上最近团队扩招,好多新人入职,当前的项目状态无异于给新同学增加了额外的学习成本。
所以统一技术栈的事情迫在眉睫。本篇就介绍一下我是如何做状态管理工具的选型的。
背景
- 团队内:
- 状态管理库较杂,主要使用Redux+thunk和Dva两个框架,且存在混用的场景,新人熟悉项目有一定的阅读成本
- Redux+thunk的代码量较长,不够精简
- Dva的generator方式不易理解,使用方式不规范
- 团队外:
- 到餐B&C端,状态管理库选型不一致(如:Redux+thunk、noflux等),影响B&C融合后研发框架共建、标准化落地等
目标
- 统一到餐B&C端状态管理库
- 结合研发框架,沉淀最佳实践,帮助研发提效
衡量标准
- 解决现有问题:
- 代码精简
- 异步处理易于理解
- 保持现有优势:
- 生态成熟:DevTools配套、最佳实践
- 稳定
方案选型
状态管理目前存在三大流派:
Redux :dva、rematch
Mobx:mobx
Hooks+Context
分别从每个流派挑选了Dva、Rematch、Mobx、Recoil(官方状态管理工具) 进行优劣对比,最终确定最优的方案:
契合度 等级从低到高 依次
⭐️
⭐️⭐️
⭐️⭐️⭐️
⭐️⭐️⭐️⭐️
⭐️⭐️⭐️⭐️⭐️
Redux(dva)当前代码 | Mobx | Recoil | Hooks+context | Rematch ( width immer ) @rematch/core | |
---|---|---|---|---|---|
设计理念 | 单一数据源,使用纯函数修改状态 提供可预测的状态管理 | 简单、可扩展的状态管理 任何可以从应用程序状态派生的内容都应该派生 操作简单、无冗余代码 |
颗粒式的state,方便共享,保持高性能 | Hooks 原生的数据管理方案 | rematch 是 redux 无样板代码的最佳实践 |
代码风格(store配置,状态修改,依赖收集,衍生数据) | redux 样板代码 | 无任何风格或者样板代码。直接的逻辑操作。 | 1、store 配置需要按照格式配置atom、selector 2、状态修改只能使用hooks api更改状态。 |
hooks 风格 | 与dva组织代码风格相似 |
调试能力 | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️ mst(⭐️⭐️⭐️⭐️⭐️) | 较完善的扩展程序:recoilize,但有点小bug。⭐️⭐️⭐️ | - | ⭐️⭐️⭐️⭐️⭐️ |
学习成本 | ⭐️⭐️⭐️ | ⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️ | ⭐️⭐️⭐️ |
TS支持度 | YES | YES | No 目前0.0.1 | Yes | Yes |
Hooks支持度 | YES | YES | YES | Yes | Yes |
类组件支持度 | YES | YES | No | - | Yes |
社区属性 | 社区维护 | 社区维护 | 官方维护 | 官方维护 | 社区维护 |
活跃度(最后一次更新) | 2 years ago | 2 days ago | 3 months ago | - | a year age |
流行度 | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️ 上升趋势明显 | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️ |
包体积 ( minzipped ) | 36k | 15.5k | 14k | 0 | 6.8kb |
demo 代码行数 | 225行 | 142行 | 没有最佳实践,暂无法统计 | - | - |
相对于dva有哪些提升(代码维度) | - | 移除了 所有redux的样板代码 | 移除了 所有redux的样板代码但同时增加了多个api 用来消费 state。但整体代码量有大幅减少 | 1、rematch( with immer ) 解决了 reducer 修改深层级对象修改值多层解构的问题.2、无sagas,使用async/awiat、3、实现了方法调用的最短链路,解决了dispatch({type: '', payload: ''})这种较复杂的形式 |
为了能对比更贴近于真实的业务。所以选取了一个页面【会员卡页面】,下面详细通过Demo,对每个状态库代码风格维度详细展开,页面效果如下:
功能点:
- 活动效果的数据展示
- tab的切换
- 默认展示有数据的tab
- table的数据展示
- 分页功能
细化维度对比
下面从store配置,状态修改,精准渲染,衍生数据四个方面详细展开对比各状态管理库代码风格
Store 配置
store配置 | dva | mobx | rematch | recoil |
---|---|---|---|---|
支持多store | Y | Y | Y | Y |
无根Provider & 使用处无需显式导入 | N | |||
store数据或方法无需人工映射到组件 | N | Y | N |
总结:
Dva 和 Rematch 同为 Redux 流派,所以无法免去 【根组件】 和 【显式导入数据映射】
Recoil 全部符合
Mobx 全部符合 - MPA的写法可以不用Provider
分析(对比dva):
1、Mobx和Recoil 可以定义多个store,做到state拆分。dva、rematch 秉承 redux规范,只能定义一个全局的store,无法拆分
2、Mobx和Recoil 不需要Provider,因为其不走props的路线。
3、对于数据和方法,Mobx 是即来既用。 Recoil 只处理状态管理,不负责方法,selector的 get 需要 hook导入使用。
Dva代码:
// index.ts
import model from './model';
const app = dva();
app.use(createLoading()); // 添加dva-loading
app.model(model);
app.start('#root');
// model.ts
const model = {
namespace: 'model',
state: {},
reducers: {},
effects: {}
}
Rematch代码:
// createStore
const store = init({
models,
plugins: [immerPlugin()],
});
const App = ({ model, update }) => {
const { a } = model
const onClick = () => {
update(1)
}
return(
<div onClick={onClick}>{a}</div>
)
}
export default connect(
(state) => ({
model: state.model,
}),
dispatch => ({
update: dispatch.model.update,
})
)(App);
Mobx代码:
// mobx
class Store {
constructor() {
makeAutoObservable(this);
}
state1 = '111';
}
class Store2 {
constructor() {
makeAutoObservable(this);
}
state2 = '222';
}
const store1 = new Store();
const store2 = new Store2()
function A(){
// 可以直接使用,无需显式导入
return(
<div>
{store1.state1} // 111
{store2.state2} // 222
</div>
)
}
Recoil代码:
// Recoil 多个 atom、selector。hooks写法直接使用。
const atom1 = atom({
type:'atom1',
default:'111'
})
const atom2 = atom({
type:'atom2',
default:'222
})
const App = () =>{
const a = useRecoilValue(atom1);
const b = useRecoilValue(atom2);
return (
<div>{a}</div> // 111
<div>{b}</div> // 222
)
}
状态修改
状态修改 | dva | mobx | rematch(width immer) | recoil |
---|---|---|---|---|
状态可追溯 | Y | N (mst Y) | Y | N |
最短链路(a.b.c = 1) | N | Y | Y | Y |
原子拆分&合并提交 | N | Y | N | Y |
总结
- Dva 在 状态修改方面无法最短链路;Mobx、Recoil可以;rematch在immer加持下可以
- Mobx、Recoil 因其store 可拆分所以符合。 Dev、Rematch 不符合
- Dva、Rematch 状态可追溯,Mobx不可以
分析(对比dva):
- Mobx 因其 mutable的写法,数据可追溯在不需要其他辅助库下无法实现、Recoil snapshot 可以。
- Mobx、Recoil 可以最短链路操作state,rematch在immer加持下可以。dva 不可以。
- mobx、recoil 可以做state拆分。dva 、immer 不可以。
Dva代码:
// model.ts 多个model时使用namespace区分
const model = {
namespace: 'model',
state: {
name: 'Hello World',
other: {
name: 'Hello Dva'
}
},
reducers: {
updateName(state, action) {
return {
...state,
name: action.payload
}
},
/** 如果嵌套层级较深,则需要多次解构 */
updateOther(state, action) {
const {other} = state;
return {
...state,
other: {
...other,
name: action.payload
}
}
}
},
effects: {
/* 异步获取数据 */
*fetchData(action, {put, call} ) {
const name = yield call(fetch)
/** 如果是修改其他model里面的值,需要添加namespace,{type: 'model/updateName'} */
yield put({ type: 'updateName', payload: name })
}
}
}
Rematch代码:
// model
const model = {
state: {
a: {
b: {
c: 0
}
}
},
reducers: {
update(payload) {
state.a.b.c = 1
return state
}
},
effects: dispatch => {
async fetch(){
const res = await request(url)
dispatch.model.update(res)
}
}
}
Mobx代码:
import { makeAutoObservable, configure } from 'mobx';
import { observer } from 'mobx-react';
// 严格模式
configure({
enforceActions: "observed",
})
class Store {
constructor() {
makeAutoObservable(this);
}
state1 = '111';
onChange = ()=>{
this.state1 = '222'
}
}
const store = new Store();
const App = observer(() =>{
return (
<div onClick={store.onChange}>
{store1.state1} // 111
</div>
)
})
Recoil代码:
import {atom, useRecoilState} from 'recoil';
const state = atom({
key:'state',
default:'111'
})
function App() {
const [a,setA] = useRecoilState(state)
onChange(){
setA('222')
}
return (
<div onClick={onChange}>
{store1.state1} // 111
</div>
)
}
精准渲染
- | dva | mobx | rematch | recoil |
---|---|---|---|---|
精准渲染 | N | Y | N | Y |
总结分析:Recoil、Mobx 都可以精准的渲染,不需要做额外的优化,且花费API较少。
分析(对比dva):
- Mobx 观察者模式 可以直接渲染被观察的组件。Recoil 依赖收集,setState({}),可以做到精准渲染。
- Dva、Rematch 走props的路线。非优化下,不可满足。
Dva代码:
// Home.tsx 组件中使用
const Home = () => {
const name = useSelector(state => state.model.name) /** 获取store中的值 */
const dispatch = useDispatch() /** 获取dispatch */
// 通过reducer修改name
dispatch({ type: 'model/updateName', payload: 'World' })
// 通过effects提交请求
dispatch({ type: 'model/fetchData' })
// 如果在请求接口期间需要loading,可以直接获取store里面的loading值,使用dva-loading注入
// 这个useSelector可以和上面的useSelector合并
const loading = useSelector(state => state.loading.effects['model/fetchData']) // 或者获取全局的loading state.loading.global
// ...
}
Mobx代码:
// mobx6 精准渲染 需要两个api
class Store {
constructor() {
// 自动observable
makeAutoObservable(this);
}
data1 = 111
data2 = 222
}
const store = new Store();
const App = observer(() => {
useEffect(()=>{
store.data1 = 333;
// App会刷新
// App2不会刷新
})
...
return (
<div>{store.data1}</div>
)
})
const App2 = observer(() => {
...
return (
<div>{store.data2}</div>
)
})
Recoil代码:
const state1 = atom({
key: 'state1',
default: '111',
});
const state2 = atom({
key: 'state2',
default: '222',
});
function App() {
const [s1,setS1] = useSetRecoilState(state1)
setS1('333') // App 会渲染, App2 不会
}
function App2() {
const [s2,setS2] = useSetRecoilState(state2)
}
衍生数据
衍生数据 | dva | mobx | rematch | recoil |
---|---|---|---|---|
自动维护计算结果之间的依赖 | N | Y | N | Y |
触发读取计算结果时收集依赖 | N | Y | N | Y |
总结:
- Mobx 和 Recoil 可以自动依赖触发.
- Dva 和 Rematch 需要自己实现逻辑。
分析(对比dva):
- Mobx、Recoil 官方默认提供computed的功能。
- Dva、Rematch 需要自己实现。
Dva/Hook/Rematch 代码:
// dva中没有类似于vue中的计算属性,可借助于useMemo实现
// Home.tsx
const Home = () => {
const [name, otherName] = useSelector(state => [state.model.name, state.model.other.name])
// names会根据name、otherName的变化而变化
const names = useMemo(() => name + otherName, [name, otherName])
}
Mobx代码:
class Store{
constructor() {
makeAutoObservable(this);
}
a = 1;
b = 2;
get getData(){
// a或b 只要有改变,getData都会执行
return this.a+this.b;
}
}
Recoil代码:
const state1 = atom({
key: 'state1',
default: '111',
});
const state2 = atom({
key: 'state2',
default: '222',
});
const selectorModel = selector({
key:'selector',
get:({get})=>{
// state1、state2 只要有改变,当前selector 都会执行。
const s1 = get(state1);
const s2 = get(state2);
return s1+s2
}
})
入门上手成本
dva | mobx | rematch | Recoil | |
---|---|---|---|---|
入门开发最少API数量(触发render、异步、同步等) | 6 | 0 | 2 | 6+ |
总结 | 项目中已使用 | mobx 3、4、5- :理解了observable、 computed、 reactions 和 actions的话,说明对于 Mobx 已经足够精通了,在你的应用中使用它吧!Mobx6 会更少 | rematch 只有一个API, 加 immer 一个,以及redux 2个常用相关(Provider, connect);相比较其他,从dva横向迁移改造成本最小 | 上手成本高,需要搞懂每个api的用法;灵活性导致无法清晰的知道用哪个api |
api数量 | 6 |
优缺点
dva | mobx | rematch | Recoil | |
---|---|---|---|---|
优点 | 6个API,易学易用;elm 概念, 通过 reducer, effects 和 subscriptions 组织model;插件机制, 如 dva-loading;支持HMR | 1.学习成本少,基础知识非常简单;2.简洁没有样板代码;3.轻松的异步;4.通过 makeAutoObserver 自动识别动作、computed 等5.高性能(局部更新),拆分越多性能越高.jpg | 1个API,无dva样板代码,易学易用;elm 概念, 通过 reducer, effects 和 subscriptions 组织 model;插件机制, 如 rematch-immer, rematch-loading;可以利用redux丰富的生态 | 颗粒式的atom,让state更加灵活;切片式的依赖插入方式,让组件的render 更高效 |
缺点 | reducer 修改深层级数据开发不爽 saga 异步数据学习成本高 | 体积较大 灵活 | 与dva的能力模型相似(本身借鉴dva) | 目前处于开发阶段,还没有正式版本;没有TS实现。目前不支持.;目前不支持Class 组件。(这个特性是必备的,应该不会抛弃Class组件,后续版本应该会支持);API偏多,有一定的上手成本。;没有代码的最佳实践,上手成本高 |
备注 | 灵活(优点):写起来很爽;过于灵活(缺点):需要集中注意力处理依赖的问题。可以通过严格模式来约束。 |
最终汇总
讨论进行到目前的程度,Dva是被改造的old code,Recoil 因未成熟目前不考虑。所以最终方案从 Rematch 和 Mobx中考虑。
积分式对比。Y:1 N:0 , 暂未考虑权重比。生成如下。
Rematch | Mobx | 备注 | |
---|---|---|---|
精准渲染 | 0 | 1 | |
衍生数据 | 0 | 2 | |
状态修改 | 2 | 2 | |
store配置 | 1 | 3 | |
总计 | 3 | 8 |
对比结果: Mobx > Rematch
基于衡量标准对比
Dva (对比基准) | Rematch | Mobx | Recoil | |
代码精简度 | ⭐️ | ⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️⭐️ |
异步易于理解 | ⭐️ effect( generator ) | ⭐️⭐️⭐️effect( async ) | ⭐️⭐️⭐️⭐️⭐️ 并不区分同步异步代码 | ⭐️⭐️⭐️⭐️⭐️ 并不区分同步异步代码 |
生态成熟 | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️ |
DevTools配套 | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️ | ⭐️⭐️⭐️ |
最佳实践 | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️ |
稳定 | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️ |
总结 | 5 | 5.5 |
对比结果: Mobx > Rematch
最终
调研+团队内的投票,选取了mobx作为团队内状态管理的选型。
文章评论