实现一个最简易 React 原子化状态管理 hooks
前言
最近看 jotai
的源码有感,发现其实在 React 中实现状态管理并不是一定要用 createContext
+ useContext
的形式,主要是这样实现的状态管理,一来必须要在祖先节点嵌入类似 Provider ,还得统一在 Provider 中写入一些更新状态的方法给子组件调用,这样写下来代码就容易变得 耦合。
原本 hooks 的目的是为了更好的业务代码解耦,使开发者可以分离各个部分的关注点从而获得更佳的开发体验。又因此变成了一团乱麻,组件之间强绑着业务关系。
所以我个人而言一直不喜欢在业务中使用这种 api,无论是使用 React 还是 Vue,Provider 模式只适合写一些公共多层组件时,像 Form 配合 FormItem、RadioGroup 配合 Radio 这种组件模式就需要使用 Provider。
在这里我分享另外一种你可能从来没见过的 React 状态管理实现方式,在 jotai
中,他们被称之为 atoms(原子状态),并实现一个简易的原子化状态管理器。
在 React 中实现状态管理的关键
大家都知道使用状态管理的目的是为了各个祖孙组件、兄弟组件更方便通信,搬一张经典的图就是这样
但是这只是浅显的说明了状态管理的机制和便利,并不能表示出一个状态管理器应该怎么去实现,我理解在 React 中实现一个状态管理器应该做到的两点关键点——
- 一个位置定义,任意位置的节点使用
- 状态更新,会触发更新所有订阅该状态的节点
但就像前言说的,为了达到关注点分离的目的,我更希望不需要使用 Provider 的形式来入侵我们的代码,这样意味着你在复用一些组件的时候,还得考虑在使用这个组件时要在祖先节点嵌入该组件使用的 Provider 以及是否存在多个 Provider 的情况。
想想看,如果用这种方式做原子化状态,那你可能需要超级多的 Provider,管理就更麻烦了。
实现原子化状态管理的关键点分析
对于第一点,与传统 redux mobx 的状态管理器不同,原子化状态管理需要实现在任何位置声明或者导出之后,直接在组件里就能使用。如果需要各种 Root 或者 HOC 来辅助使用,便会加重开发使用负担。
export const bar = createStore('xekin')
function App () {
const [bar, setBar] = useStore(bar)
console.log(bar) // 'xekin'
// ....
}
关键是第二点,如何能做到触发状态后更新所有订阅该状态的组件。
这就要关系到 React 的更新机制了,其实很简单,调用一下 setState
就可以触发整个组件更新了,React 和 Vue 不同,React 组件更新是全量更新,这导致在 React Element 中每一个 state 都无法与整个组件剥离更新。于是我们只要在每个组件使用 useStore
之类的方法时,在方法里存下该组件的一个 setState
方法即可。
存入之后,还需要提供修改的 setStore
方法,并且在 setStore
时,触发所有之前存入的 setState
方法。
于是我们只需要这样——
function useUpdate () {
const [,setState] = useState(Date.now())
const update = useRef(() => setState(Date.now()))
return update.current
}
function useStore () {
const store = {}
const update = useUpdate()
const setStore = useRef(() => {
// ...modify store value
updates.forEach((func) => func())
})
useEffect(() => {
saveUpdate(update)
return () => {
deleteUpdate(update)
}
}, [])
return [store, setStore]
}
这就是基本的原子化 store 雏形了,这种方式其实是得益于 React 自身的组件更新机制。
具体实现
来看一下具体的实现, 一共也就 45 行代码
import { useEffect, useRef, useState } from 'react'
type Update = () => any
type Store<T = unknown> = { value: T; _storeDeps: Set<Update> }
function createStore<T = unknown>(value: T): Store<T> {
return {
value,
_storeDeps: new Set(),
}
}
function effectStore(store: Store) {
store._storeDeps.forEach((effect) => {
effect()
})
}
export function useUpdate() {
const [, refresh] = useState(Date.now())
const update = useRef(() => {
refresh(Date.now())
})
return update.current
}
export function useStore<T = unknown>(store: Store<T>) {
const update = useUpdate()
const setState = useRef((newValue: T) => {
store.value = newValue
effectStore(store)
})
useEffect(() => {
store._storeDeps.add(update)
return () => {
store._storeDeps.delete(update)
}
}, [])
return [store.value, setState.current]
}
看到这有聪明的小伙伴就要说了,在使用 useContext
的时候,一旦某个状态更新,所有订阅该 Context 的组件都会更新,导致产生很多不必要的 rerender,你这样写不也一样嘛,我所有用 useStore
的组件都会可能因为某个与这个组件无关但是因为订阅了该 store 而被更新。
其实注意理解这个过程,你会发现如果每个 store 都只存一个状态,那这个 store 自然就变成一个原子化的 state 了,我们并不需要所有状态都存进一个 store 里。这样一个 store 就是一个 state,就没有区分不同 state 触发更新的问题了。
可以看看下面使用起来会是什么样。
使用
// store.ts
import { useStore } from 'store';
export const nameState = createStore('tom');
export const arrState = createStore([1,2,3]);
// app.tsx
import { nameState, arrState } from 'store.ts'
function Bar () {
const [name, setName] = useStore(nameState)
useEffect(() => {
setName('xekin')
}, [])
return (<div>{name}</div>)
}
function Foo() {
const [arr] = useStore(arrState)
return (<div>{arr.map(item => ( <span>{item}</span> ))}</div>)
}
function Car() {
const [name] = useStore(nameState)
const [state, setState] = useState('')
useEffect(() => {
setState(name)
}, [name])
return (<div>{state}</div>)
}
function App() {
return (
<Bar />
<Foo />
)
}
拓展性
其实这个状态管理器的雏形有着非常多的拓展方向,比如可以结合 localStorage 或者 url 做成持久化状态管理,代码量也完全不需要改动多少,相比 Provider 模式更加轻便、好理解。
举个很有意思的例子,把异步接口和状态融合在一起,比如这样
const api = (id: string) => fetch('/xxxx?id=' + id)
const apiValue = createStore({})
function compose (api, store) {
return () => {
const [state, setState] = useStore(store)
const getValue = (...args: any[]) => {
api(...args).then(data => {
setState(data.json())
})
}
return [state, getValue]
}
}
const useApiState = compose(api, apiValue)
function App() {
const [value, get] = useApiState()
useEffect(() => {
get('xekin')
}, [])
return (<div>{value}</div>)
}
这样还可以直接在最上层对调用接口进行处理,例如节流防抖等等,用起来绝对爽爆。
One More Thing
比起一些业务组合,我其实发现一个更有意思的点,那就是这种状态是可以脱离 react 上下文去更新的。
const nameState = createStore('xekin')
window.addEventListener('message', (e) => {
nameState.value = e.data
effectStore(nameState)
})
function App() {
const [name] = useStore(nameState)
return <div>{name}</div>
}
最后
这里是 Xekin(/zi:kin/),以上这就是本篇文章分享的全部内容了,喜欢的掘友们可以点赞关注点个收藏~
最近摸鱼时间比较多,写了一些奇奇怪怪有用但又不是特别有用的工具,不过还是非常有意思的,之后会一一写文章分享出来,感谢各位支持。
我还是喜欢写没人写过的东西~
转载自:https://juejin.cn/post/7200609608414412860