愚墨的博客
  • 首页
  • 前端技术
  • 面试
只争朝夕不负韶华
  1. 首页
  2. 前端技术
  3. 正文

React状态管理工具选型

2021年02月23日 2445点热度 0人点赞 0条评论

最近梳理了团队内的前端项目,发现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,对每个状态库代码风格维度详细展开,页面效果如下:

功能点:

  1. 活动效果的数据展示
  2. tab的切换
  3. 默认展示有数据的tab
  4. table的数据展示
  5. 分页功能

细化维度对比

下面从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作为团队内状态管理的选型。

标签: 暂无
最后更新:2023年07月23日

愚墨

保持饥渴的专注,追求最佳的品质

点赞
< 上一篇
下一篇 >

文章评论

取消回复

搜搜看看
历史遗迹
  • 2023年5月
  • 2022年9月
  • 2022年3月
  • 2022年2月
  • 2021年12月
  • 2021年8月
  • 2021年7月
  • 2021年5月
  • 2021年4月
  • 2021年2月
  • 2021年1月
  • 2020年12月
  • 2020年11月
  • 2020年9月
  • 2020年7月
  • 2020年5月
  • 2020年4月
  • 2020年3月
  • 2020年1月
  • 2019年5月
  • 2019年3月
  • 2019年2月
  • 2019年1月
  • 2018年9月
  • 2018年3月
  • 2018年2月
  • 2018年1月
  • 2017年11月
  • 2017年7月
  • 2017年6月
  • 2017年3月
  • 2017年2月
  • 2017年1月
  • 2016年12月
  • 2016年11月
  • 2016年9月
  • 2016年8月
  • 2016年7月
  • 2016年6月
  • 2016年5月
  • 2016年4月
  • 2016年3月
  • 2016年2月
  • 2016年1月
  • 2015年12月
  • 2015年10月
  • 2015年9月
  • 2015年7月
  • 2015年6月
  • 2015年4月

COPYRIGHT © 2020 愚墨的博客. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS