轻量级的React状态管理——co-store

2,172 阅读8分钟

背景

由于react追求通过组合不同粒度的组件来展示视图,对于功能复杂的应用来说,组件间逻辑复用状态共享的问题油然而生,对于逻辑复用,react从render props+HOC到custom hooks,基本可以说是十分契合react基于组合的编程模型了;对于状态共享,当下最火的方案,当属基于redux和mobx的全局状态管理方案了,但在hooks出现后,基于hooks和context的方案在社区中的呼声亦不容小觑,就在这样的背景下,一个基于该方案的轻量级的react状态管理——co-store诞生了!下面就一起走进科学,看看这几种方案在服务react项目中的对比吧~

Part1:设计与接入

redux

  1. 设计

    Redux attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen(Redux试图通过对更新的方式和时间施加一定的限制来使状态突变可预测)

    围绕这个思想,redux设计了下面几个核心概念

    • store:“存”,是集中保存状态的地方
    • action:“改”,一个普通的js对象,是改变状态的唯一办法
    • reducer:“变”一个纯函数,根据action完成改变store中对应的状态
    • middleware:“强”,中间件,可以理解成对action预处理,也可以理解成增强store

redux流程图

  1. 接入

    由于redux是与框架无关的全局状态管理方案,在react应用中接入必须配合react-redux

    // 创建
    const rootReducer = (state, action) => {
      switch (action.type) {
        case 'XXX':
          return newState
        default:
          return state
      }
    }
    const store = createStore(rootReducer);
    
    // 接入React
    <Provider store={store}>...</Provider>
    

mobx

  1. 设计

    Reacting to state changes is always better then acting on state changes.(对状态变化做出反应总是比对状态变化采取行动更好)

    围绕这个思想,mobx设计了下面几个核心概念

    • state: 驱动应用的数据,像是有数据的excel表格
    • derivations: 任何源自状态并且不会再有任何进一步的相互作用的东西就是衍生
      • computed values: 计算值,从当前可观察状态中衍生出的值, excel的公式就是计算值的衍生
      • reactions:当状态改变时需要自动发生的副作用
    • action: 任一一段可以改变状态的代码,像在excel单元格中输入一个新的值就可以称为action

mobx流程图

  1. 接入

    由于mobx是与框架无关的状态管理方案,在react应用中接入必须配合mobx-react(或者mobx-react-lite)

    // 创建
     const store = () => useLocalStore(() => {
      return {
        ...state,
        ...actions
      }
    })
    
    // 接入React
    <Provider stores={stores}>...</Provider>
    

co-store

  1. 设计

    co-store attempts to define store according to function to converge state as much as possible.(co-store尝试根据功能定义store来尽可能地收敛状态)

    co-store类比custom hooks,强调以功能为最小单元去创建store,当且仅当你的功能需要在全局或组件间共享状态时才有必要去定义store,否则尽可能地收敛状态到对应的组件内。然后通过组合组件去生成UI,通过组合hooks去定制逻辑。

    围绕这个思想,co-store设计了下面几个核心概念

    • state:存储store的所有状态
    • action:是改变状态的唯一办法
    • subscription:支持选择性订阅状态和衍生数据

co-store流程图

  1. 接入

    // 创建
    const initialState = xxx
    const actionCreators = {
      xxx: () => xxx
    }
    const store = createScopeStore(initialState, actionCreators)
    
    // 接入React
    <store.Provider>...</store.Provider>
    

summary

设计与接入reduxmobxco-store
不需要配合库react-reduxmobx-react(mobx-react-lite)
支持拆分store
思想使状态突变可预测对状态变化做出反应尽可能地收敛状态

Part2:状态读写

redux

通过定义mapStateToProps和mapDispatchToProps作为react-redux提供的connect方法的参数,把redux中store中的state和dispatch作为props传递到组件中。换句话说,react中的redux,通过props实现状态读写。

const mapStateToProps = state => ({...})
const mapDispatchToProps = dispatch => ({...})
const Component = (props) => (...)
export default connect(mapStateToProps, mapDispatchToProps)(Component)

mobx

通过Context把定义的stores组合起来,通过useStores(基于useContext)来获取对应的store,通过包装useObserver(mobx-react-lite)来订阅状态,通过直接调用store中属性(包括state和action)来改变状态,mobx会对变更的状态做出正确的响应。换句话说,react中的mobx,通过被观察的对象与this来实现状态读写。

const {xxxStore} = useStores()
const Component = (props) => useObserver(() => (...))
export default Component

co-store

每一个store都具备特定的功能,直接引入,通过store提供的useStore、useActions来实现状态的读写。

const [state, actions] = xxxStore.useStore()
const Component = (props) => (...)
export default Component

summary

状态读写reduxmobxco-store
无this
状态不可变
不需要手动映射
原生支持异步

Part3:选择性订阅和衍生数据

redux

redux结合react-redux实现的状态管理本身并不支持选择性订阅和衍生数据,状态作为props传入组件,需要组件本身去做优化。

mobx

mobx通过对组件包装useObserver,可以实现只有被订阅的state节点变化时,才进行对应的更新,这样就实现了选择性订阅;在定义store时通过定义属性的get方法来实现衍生数据(计算属性)。

// 当xxxStore.state1的变化时,组件才会进行对应的更新
const {xxxStore} = useStores()
const Component = (props) => useObserver(() => (...xxxStore.state1...))

// 衍生数据(计算属性)computedState1,当依赖的state1变化时会进行对应的更新
const store = useLocalStore(() => {
  return {
    ...state,
    get computedState1(){
    	return this.state.state1...
    }
    ...actions
  }
})

co-store

与通过功能定义store的思想一致,co-store需要在创建时提前声明subscriptions,然后使用时通过store提供的useSubscribe来实现选择性订阅的状态和衍生数据

// 选择性订阅的状态和衍生数据
const subscriptions = {
  // 只订阅了state1,只有state1改变了才会重渲染
  state1: (state) => state.state1,
  // 订阅了computedState1,当依赖的state1变化时才会重渲染
  computedState1: (state) =>  state.state1...
}
const store = createScopeStore(initialState, actionCreators, subscriptions)

// 使用
const state1 = xxxStore.useSubscribe("state1")
const computedState1 = xxxStore.useSubscribe("computedState1")

summary

选择性订阅和衍生数据reduxmobxco-store
无this
定制更新
衍生数据(计算属性)

Part4:性能对比

性能对比其实非常麻烦,因为很难找到一个标准去衡量,查找了不少资料,觉得Mobx 与 Redux 的性能对比这篇文章的对比场景还是可以借鉴的,下面就基于这个场景,直接对比一下在1000个组件同时订阅同一个store场景下mobx和co-store的耗时:

  • mobx 5次平均耗时:10.6538s
  • co-store 5次平均耗时:10.0624s

不难看出,如果针对上述的场景,mobx和co-store的表现没有太大的差别(可以假装得出co-store≈mobx > redux的结论),这里的对比其实也是针对场景,不算很严格,只是为了说明基于Context实现的store在性能上也不差,毕竟是react官方出品~

Part5:实战·Todos

redux

直接奉上官网的例子:redux-todos

mobx

codesandbox:mobx-todos

co-store

codesandbox:co-store-todos

写在最后

基于redux和mobx的全局状态管理方案,推荐把状态都放在store去管理,从某种程度来说,与React中custom hooks根据功能定制逻辑的思想似乎不太契合;现在也有不少将custom hooks封装成store的库,大概的思路就是去每个组件都有自己的状态,当一个组件状态更新时,通过context+发布订阅模式去决定其他组件是否需要更新,这样做的好处是简介明了,但是store概念不太清晰,功能复杂一点的hooks很有可能把状态和副作用以及其他逻辑冗杂在一起,另外,拆分多个hooks去组合store,在store相互依赖时需要手动维护对应Provider的顺序问题;

说了这么多不好,按照套路,这时候应该宣传一波co-store了!然而,co-store的理念是尽可能地收敛你的状态到对应的组件中,换句话说,在写store之前先确定你是否真的需要它,根据需求场景和功能去决定你是否需要状态管理,如果需要,再去寻求解决该场景的最佳方案,如果需求场景(如应用设计层)迫切地需要event sourcing这种时间回溯能力,redux是一个很不错的选择;如果你习惯了MVC那套抽离view和数据层,并且在this的海洋穿梭自如,或许基于功能的声明式的store的不是你的菜,mobx在这方面的造诣已经很高了。

而对于co-store,它的定位就是react大力推广的hooks+function component模式(其实通过封装,也可以支持class component),正如背景所说,co-store解决的不是逻辑封装的问题,而是状态共享的问题,根据功能封装你的逻辑,根据功能定义你的store,我想这种基于这种模式去组合声明式的UI,或许是react在践行代数效应里的最佳实践吧~

关于代数效应,我安利大牛的代数效应与React这篇文章。另外,如果你对co-store感兴趣,奉上源码传送门,欢迎一起探讨交流~