基于react context构建模块可引用store

879 阅读5分钟

想要在当前页面相关任意函数中获取或设置你的react store中的数据?Have A Try

背景

react的状态管理主要有两种形式:

  • 局部状态管理(useState)
  • 顶部状态管理(useContext)

当项目比较简单时,一般使用局部状态管理就够了;

当项目变复杂,比如多个嵌套组件需要共享状态时,为了避免出现嵌套传参的场景(即将参数一层层下传),官方提供了两种方案:

这两种方案的优缺点官方已经说明了,其中使用context虽然更简单,但会使组件复用性降低,原因是需要复用这个组件的地方必须要有一个context来提供这部分组件需要的数据。

比如下面两个组件相比,ComponentB比ComponentA的复用性更高,因为ComponentB只需把数据data传入即可,而ComponentA却需要构建一个react context包裹。

const ComponentA = (props)=>{
    const data = useContext(context)
    return <A data={data}></A>
}
const ComponentB = (props)=>{
    const data = props.data
    return <A data={data}></A>
}

然而,在实际开发中,我们并不一定需要选择复用性高的方式,相反,我们在开发偏大型项目时,往往很多逻辑很多组件都是独有的,是无需复用的,或者说我们当下感知不到该组件或该逻辑需要被复用,在这种场景下,我们使用context能够很方便地进行开发,props的参数能大量减少,因为很多数据我们可以直接从context里面直接获取。

因此,在开发偏大型项目时,我们很喜欢用context来储存公共数据,然而,context在我个人看来并不足够自由,context有一定的规则限制我们的滥用:

1、context要遵循hooks规范,只能在组件或hooks中使用,在普通函数中无法使用,也就是说在普通函数中,如果要获取到context中的数据,需要用参数传进来,或者把它放到hooks中

2、context只管数据传递,如要修改数据,修改的方法我们也要传递下去

基于以上两点,我们其实可以创建一个自由度更高的store,即可通过模块引入的store,但是也需要付出一定的代价

方案

  • 将store构建为模块变量,脱离react context语法限制,具体如下

要在任意函数中获取数据,其实很简单,只需要将store构建为模块变量即可,这样就可以实现哪里需要往哪里引。这里实际上要解决的问题是怎么跟react联动起来,即数据变更时,react能触发重新渲染。

这里用的办法是将react useState的data和setData方法提升至模块实例中

/**
   * store的Provider,即react的Provider
   * 将其包裹在顶层可在更新数据时更新视觉
   */
  Provider = ({
    children,
  }: {
    children: ReactElement | (() => ReactElement)
  }) => {
    const [data, setData] = useState(this._initData)
    //修改数据方法往上提升至实例变量中
    this._setData = setData
    //数据也提升至实例变量中
    this._data = data
    useEffect(() => {
      //页面离开后,解除对store的引用
      return () => {
        Store.map.delete(this._id)
      }
    }, [])
    return (
      <this._context.Provider value={data}>
        {typeof children === 'function' ? children() : children}
      </this._context.Provider>
    )
  }

这样,我们就可以获取到useState的setData方法,我们再基于此构建自己的update方法:

update<Return = void>(fn: (draft: Draft<Data>) => Return) {
    //通过immer来更新数据,暂不支持异步更新
    return this._setData((oldData) => {
      const newData = produce(oldData, fn) as Data
      if (newData instanceof Promise) {
        throw Error('Async update is not supported now')
      }
      return newData
    })
  }

在此,我们引入immer来进行数据修改,进一步提升更改数据的便捷性,即使store里数据再复杂,我们也可以像修改引用一样修改它:

const store = new Store({a:2,b:4})
store.update(draft=>{
	draft.a++
})

然后,你就可以在任意的函数中使用你的store了:

import { store } from ''

const test = ()=>{
    store.update(draft=>{
    	draft.a++
    })
}

唯一要注意的一点是,context牺牲了一部分复用性,而上面的store则会导致你的组件和函数复用性变得更差,这就是上面说的代价,请有心理预期并在合适的场景中使用,

进一步优化

基于上面的store我们已经实现了能够在任意地方引用store,但是仍然有以下问题:

  • store需要我们手动管理,在页面挂载时需要清空或删除store,页面加载时需要初始化或创建store,这是非常麻烦且容易出错的
  • 将store构建为模块变量后,会大大占用内存,往往一个页面可能只用到了一个store,但是单页应用中加载了其他页面后其他的store变量也存在内存中不会被销毁,有很大的隐患

基于这些问题,我们提供一个SingletonStore抽象类,可通过继承该类来构建自己的Store,该类提供静态get方法,可直接通过get方法获取store实例:

const initData = {
    a: 2
  }
class MyStore extends SingletonStore<typeof initData> {
  static initData = initData;
  static get: () => Store<typeof initData>;
}

export function App() {
  const store = MyStore.get();
  return (
    <store.Provider>
      {() => (
        <div className="App">
          <h1>Hello CodeSandbox</h1>
          <h2>Start editing to see some magic happen!</h2>
          <button
            onClick={() => {
              store.update((draft) => {
                draft.a++;
              });
            }}
          >
            增加
          </button>
          {store.data.a}
        </div>
      )}
    </store.Provider>
  );
}

这样,就能直接通过get方法获取store,在页面卸载时,则会自动删除该store实例,即无需关心store的创建与删除,只需直接使用即可。

其他扩展

该store只是一个基础的store,但是通过该store我们能够知晓数据的每一次变更,我们可基于此进行其他扩展,比如增加一些事件;储存历史数据,去完成一个数据回退的功能等。

最后,Have A Try