想要在当前页面相关任意函数中获取或设置你的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