likes
comments
collection
share

实现一个最简易 React 原子化状态管理 hooks

作者站长头像
站长
· 阅读数 27

前言

最近看 jotai 的源码有感,发现其实在 React 中实现状态管理并不是一定要用 createContext + useContext 的形式,主要是这样实现的状态管理,一来必须要在祖先节点嵌入类似 Provider ,还得统一在 Provider 中写入一些更新状态的方法给子组件调用,这样写下来代码就容易变得 耦合

原本 hooks 的目的是为了更好的业务代码解耦,使开发者可以分离各个部分的关注点从而获得更佳的开发体验。又因此变成了一团乱麻,组件之间强绑着业务关系。

所以我个人而言一直不喜欢在业务中使用这种 api,无论是使用 React 还是 Vue,Provider 模式只适合写一些公共多层组件时,像 Form 配合 FormItem、RadioGroup 配合 Radio 这种组件模式就需要使用 Provider。

在这里我分享另外一种你可能从来没见过的 React 状态管理实现方式,在 jotai 中,他们被称之为 atoms(原子状态),并实现一个简易的原子化状态管理器。

在 React 中实现状态管理的关键

大家都知道使用状态管理的目的是为了各个祖孙组件、兄弟组件更方便通信,搬一张经典的图就是这样

实现一个最简易 React 原子化状态管理 hooks

但是这只是浅显的说明了状态管理的机制和便利,并不能表示出一个状态管理器应该怎么去实现,我理解在 React 中实现一个状态管理器应该做到的两点关键点——

  1. 一个位置定义,任意位置的节点使用
  2. 状态更新,会触发更新所有订阅该状态的节点

但就像前言说的,为了达到关注点分离的目的,我更希望不需要使用 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 而被更新。

实现一个最简易 React 原子化状态管理 hooks

其实注意理解这个过程,你会发现如果每个 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
评论
请登录