为什么又要造轮子
hook 自带轮子光环
关于 react hook 我就不多介绍了。hook 提供了抽象状态的能力,自然而然让人想到可以基于 hook 抽离全局状态。其天生自带轮子光环,所以社区也出现了不少基于 hook 的状态管理工具,比如说前阵子飞冰团队出的 icestore,亦或者这个 stamen,不过相对来说我更喜欢的还是这个 unstated-next。
那既然别人都已经造了那么多轮子了,为什么自己还要造呢?自然是因为:
别人的轮子不够用
比如说 unstated-next,它本质上是把一个自定义 hook 全局化了。理念很好,可惜颗粒度太大了一点。必须把 state、actions、effects 维持在一个自定义 hook 中。内部的一系列 actions、effects 需要加 useCallback、useMemo 也比较麻烦,如果抽离到外部,又要传很多参数,写 TS 的话,还要写不少泛型。总之,如果项目相对比较复杂,写起来比较累。
stamen 其实也不错。声明一个 store,包含 state、reducer、effects。而且不需要给组件包裹 Provider,各个地方随意拔插,响应更新。就是 dispatch 我不太喜欢用,不太好直接定位到 action 或 effect 的声明,且丢了入参出参类型。
icestore 的问题也差不多。说是支持 TS,其实是残缺的,看了下源码,类型完全都丢失了。另外命名空间这一套我也不是很喜欢。
当然上述这些问题人家也能优化。但是何必呢,本来也没几行代码,给人家提 PR 的时间,我自己都写好轮子了。所以总而言之,还是自己造吧。
我的理想型
那我自己想要的状态管理工具是怎么样的呢?在 hoox 之前呢,其实我还实现了一版,基本复制 dva 的 api 的一个版本(把 yield 换成 async/await)。有点儿像 icestore,只不过没有命名空间。最致命且无法解决的问题就是丢失了类型,丢失了函数引用。
后来我总结了一下,我真正想要的是怎么样的:
- 全局状态管理,且非单一 store;
- actions 跟 effects 就是正常的函数,独立声明,直接引用;
- 完美的 TS 支持。
所以目标很简单,可以说就是 unstated-next 的去 hook 包裹版。于是我实现了一版,最终效果如下:
HooX
创建全局状态
// store.js
import createHoox from 'hooxjs'
// 声明全局初始状态
const state = {count: 1}
// 创建 store
export const {setHoox, getHoox, useHoox} = createHoox(state)
// 创建一个 action
export const up = () => setHoox(({ count}) => ({count: count + 1}))
// 创建一个 effect
export const effectUp = () => {const [{ count}, setHoox] = getHoox();
const newState = {count: count + 1}
return fetch('/api/up', newState).then(() => setHoox(newState))
// 或者直接引用 action
// return fetch('/api/up', newState).then(up)
}
消费状态
import {useHoox, up, effectUp} from './store';
function Counter() {const [state] = useHoox()
return (
<div>
<div>{state.count}</div>
<button onClick={up}>up</button>
<button onClick={effectUp}>effectUp</button>
</div>
)
}
直接修改状态
import {useHoox} from './store';
function Counter() {const [state, setHoox] = useHoox()
return (
<div>
<div>{state.count}</div>
<input
value={state.count}
onChange={value => setHoox({ count: value})}
/
</div>
)
}
重置状态
我们知道,在 class 组件中,通过 this.setState
是做状态的合并更新。但是在 function 组件中,useState
返回的第二个参数 setState
又是做替换更新。实际使用中,其实我们都有诉求。尤其是非 TS 的项目,状态模型可能是动态的,很可能需要做重置状态。为了满足所有人的需求,我也加了个 api 方便大家使用
import {useHoox} from './store';
function Counter() {const [state, setHoox, resetHoox] = useHoox()
return (
<div>
{state ? <div>{state.count}</div> : null}
<button onClick={() => resetHoox(null)>reset</button>
</div>
)
}
全局 computed
通过上述 api,其实我们还可以实现类似 vue 中 computed
的效果。
import {useHoox} from './store';
export function useDoubleCount () {const [{ count}] = useHoox();
return count * 2
}
对于某些非常复杂的运算,我们也可以使用 react 的 useMemo
做优化。
import {useHoox} from './store';
export function usePowCount (number = 2) {const [{ count}] = useHoox();
return useMemo(() => Math.pow(count, number), [count, number])
}
除此外,也可以实现一些全局 effect。
不够美好的地方
需要 Provider
hoox 底层基于 context
跟 useState
实现,由于把状态存在 context
了中,故而类似 Redux,消费状态的组件必须是相应 Context.Provider
的子孙组件。如:
import {Provider} from './store';
import Counter from './counter';
function App() {
return <Provider>
<Counter />
</Provider>
}
这进而导致了,如果一个组件需要消费两个 store,那就需要成为两个 Provider
的子孙组件。
hoox 提供了一个语法糖 createContainer
,可以稍微的简化一下语法。
import {createContainer} from './store';
import Counter from './counter';
function App () {return <Counter />}
export default createContainer(App)
其他不好的地方
留给评论区
Github
具体的源码跟 api 介绍可以见 github:https://github.com/wuomzfx/hoox
关于源码部分我就不详细说明啦,也没几行代码,看看就能明白。