前言

其實在一個多月前,我也已經在掘金髮過hoox 的介紹。不過我覺得這麼簡單的東西,也沒太大技術含量,也就是隨便發發看,圖個樂。最近發現基於 Hook 的狀態管理器越來越多了,那我也就再在知乎趕個集好了,免得以後再發顯得有點兒山寨。

另外,我還是提前說,目前我的這個小玩具,還是 0。x 的版本。我還不敢發正式版,一方面是我自己覺得還有些未完善之處。另一方面,是它確實沒有經過非常多專案的考驗。不過,如果純按業務流量來說,它已經在螞蟻保險幾個百萬~千萬 UV 級的 C 端頁面上跑了很久了。目前來看,沒有明顯異常。這也是我發文章時稍微有的一丁點兒底氣。

迴歸正文:

為什麼又要造輪子

hook 自帶輪子光環

關於 react hook 我就不多介紹了。hook 提供了抽象狀態的能力,自然而然讓人想到可以基於 hook 抽離全域性狀態。其天生自帶輪子光環,所以社群也出現了不少基於 hook 的狀態管理工具,比如說前陣子飛冰團隊出的icestore,亦或者這個stamen,不過相對來說我更喜歡的還是這個unstated-next。

那既然別人都已經造了那麼多輪子了,為什麼自己還要造呢?自然是因為:

別人的輪子不夠用

比如說unstated-next,它本質上是把一個自定義 hook 全域性化了。理念很好,就是狀態邏輯比較複雜的話,寫起來有點兒累。必須把 state、actions、effects 維持在一個自定義 hook 中。內部的一系列 actions、effects 需要加 useCallback、useMemo 也比較麻煩,如果抽離到外部,又要傳很多引數。總之,如果專案相對比較複雜,寫起來比較累。

stamen 其實也不錯。宣告一個 store,包含 state、reducer、effects。而且不需要給元件包裹 Provider,各個地方隨意拔插,響應更新。就是 dispatch 我不太喜歡用,不太好直接定位到 action 或 effect 的宣告,且丟了入參出參型別。

icestore 的問題也差不多。說是支援 TS,其實是殘缺的,看了下原始碼,型別完全都丟失了。另外名稱空間這一套我也不是很喜歡。

另外,前兩天螞蟻體驗技術部的同學也出了一個hox。名字跟我的這個很像,但確實不是一個東西。它呢有點兒像 unstated-next 跟 statemen 的結合體。按我理解,它核心就是想在 unstated-next 的基礎上,解決巢狀

Provider

的問題。不過這也不是我使用 unstated-next 時的痛點。另外,其內部使用

ReactDOM。render

來實現,沒法實現 SSR。

當然上述這些問題人家也能最佳化。但是何必呢,本來也沒幾行程式碼,給人家提 PR 的時間,我自己都寫好輪子了。所以總而言之,還是自己造吧。

我的理想型

那我自己想要的狀態管理工具是怎麼樣的呢?在 hoox 之前呢,其實我還實現了一版,基本複製 dva 的 api 的一個版本(把 yield 換成 async/await )。有點兒像 icestore,只不過沒有名稱空間。但它有著 icestore 跟 stamen 同樣的問題,不太好直接定位到 action/effect 的宣告。

後來我總結了一下,我真正想要的是怎麼樣的:

全域性狀態管理,但非單一 store;

actions 跟 effects 就是正常的函式,獨立宣告,直接引用;

完美的 TS 支援。

所以目標很簡單,可以說就是 unstated-next 的去 hook 包裹版。於是我實現了一版,最終效果如下:

HooX

建立全域性狀態

// store。js

import

createHoox

from

‘hooxjs’

// 宣告全域性初始狀態

const

state

=

{

count

1

}

// 建立store

export

const

{

Provider

// 使用全域性狀態的元件或者其根元件,需要被Provider包裹

useHoox

// 獲取全域性狀態,以及更新全域性狀態的方法,類似useState

getHoox

// 獲取全域性狀態,相比useHoox,其獲取的狀態更新時,並不會觸發元件更新,常用於effect跟action中

}

=

createHoox

state

// 建立一個 action

export

const

up

=

()

=>

{

const

[{

count

},

setHoox

=

getHoox

()

return

setHoox

({

count

count

+

1

})

}

// 建立一個 effect

export

const

effectUp

=

async

()

=>

{

// getHoox 跟 useHoox

const

[{

count

},

setHoox

=

getHoox

()

const

newState

=

{

count

count

+

1

}

await

fetch

‘/api/up’

newState

return

setHoox

newState

// 或者直接引用action

// return up()

}

當然,如果

action/effect

場景簡單的話,也有些簡單的 api。

export

const

{

// 。。。其他api

setHoox

}

=

createHoox

state

// 建立一個 action

export

const

up

=

()

=>

setHoox

(({

count

})

=>

({

count

count

+

1

}))

可以看到,透過這樣的方式,建立

action/effect

以及全域性狀態就脫離 hook 了。這樣的好處有:

action/effect

不在 hook 中,避免每次 render 導致的函式重新宣告(進而需要

useCallback/useMemo

)。

可方便的將方法抽離到其他檔案,降低單個檔案複雜度。

消費狀態

在元件裡使用全域性狀態,為了保證響應式,需要透過

useHoox

獲取。如果是使用

action/effect

,那就比較簡單了,直接引用即可。

切忌,元件不應該透過

getHoox

獲取全域性狀態,因為它不具有響應式的邏輯。雖然也能獲取到狀態,但是並不會因為狀態的變更而觸發元件 render。

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>

}

直接修改狀態

如果場景較為簡單,且不需要抽象

action

,也可以直接在元件內部更新狀態。

import

{

useHoox

}

from

‘。/store’

function

Counter

()

{

const

state

setHoox

=

useHoox

()

return

<

div

>

<

div

>

{

state

count

}

<

/div>

<

input

value

=

{

state

count

}

onChange

=

{

event

=>

setHoox

({

count

event

target

value

})}

/>

<

/div>

}

如果這個元件只更改狀態,不需要消費狀態,也可以直接用

setHoox

import

{

setHoox

}

from

‘。/store’

function

Inputer

()

{

return

<

div

>

<

input

onChange

=

{

event

=>

setHoox

({

count

event

target

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

])

}

除此外,也可以實現一些全域性 hooks。

connect

其實正常來說,我的業務程式碼基本不太會寫

connect

的。。。直接

useHoox

即可。但也有兩種情況是例外的:

引用了某些通用性的元件,想透過 connect props 來解耦全域性狀態邏輯。

一個是以前就寫好的

class

元件不想改造成

function

元件,但又要用到全域性狀態。

所以實現了

connect

這個 api,方便解決這兩個問題。

首先 store 中需要暴露出這個 api。

// store。js

export

const

{

// 。。。其他api

connect

}

=

createHoox

state

然後對於函式式元件:

// Counter。js

import

{

connect

}

from

‘。/store’

const

Counter

=

({

count

})

=>

{

return

<

div

>

{

count

}

<

/div>

}

const

NewCounter

=

connect

state

=>

({

count

state

count

}))(

Counter

export

default

NewCounter

connect

以後,返回的

NewCounter

,就不需要再接受

count

這個

prop

,這個也已經做好了型別推導。

如果想用裝飾器的話,函式元件是沒有辦法的,不過

class

可以。

import

{

connect

}

from

‘。/store’

@

connect

state

=>

({

count

state

count

}))

export

default

class

Counter

extends

React

PureComponent

{

render

()

{

return

<

div

>

{

this

props

count

}

<

/div>

}

}

但這個裝飾器僅限於 js 環境,ts 環境下,裝飾器不能改變 class 的返回型別。但是實際程式碼中,元件被

connect

後,我會返回一個新的函式式元件,並且改變了元件

Props

的型別(去除了全域性狀態注入的 props)。因此 ts 環境下,無法正常使用裝飾器。當然 使用函式包裹依舊是可以的:

import

{

connect

}

from

‘。/store’

class

Counter

extends

React

PureComponent

<

{

count

number

}

>

{

render

()

{

return

<

div

>

{

this

props

count

}

<

/div>

}

}

export

default

connect

state

=>

({

count

state

count

}))(

Counter

不夠美好的地方

需要 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

雖然這樣還是有些繁瑣。尤其當有多個 store 互相呼叫的時候,需要特殊注意,用到狀態的元件是否在相應的 Provider 包裹下。但我依舊不願意使用類似

stamen

hox

這樣釋出訂閱的方法。因為 react 已經有一套自己響應式邏輯了。再在上面加個釋出訂閱的邏輯。。。我能力比較差,hold 不住。。。 目前我也想不到更好的辦法,只能提供下語法糖,稍微簡化一點點。

幾個 api 第一次使用會分不清

getHoox

useHoox

setHoox

什麼的,確實 api 看著比較多,第一次用會有點兒懵。可能還會用錯。不過新手用只要切記一點:

沒什麼特殊要求,不要在元件裡使用getHoox。

只要牢記這點,基本只要能跑通,就沒啥大問題。

用一小段時間後,明白

getHoox

是非響應式地獲取全域性狀態,後續就 OK 了。最近團隊裡有個同學再研究

eslint-plugin

。後續讓他幫忙寫個

hoox

的 lint 外掛就能改善一部分問題了。

目前來看,我也找不到其他更好的辦法能解決這個問題,我必須有這幾個

api

其他不好的地方

留給評論區

寫在最後

這個工具,目前我們團隊內有 5-6 個人使用。整體而言,口碑還行,尤其是對於一些中小專案。有些元件稍微繁瑣一些,就會有一堆 useMemo 來,useCallback 去的邏輯。透過

hoox

,將這些邏輯抽離出 hook,程式碼會清爽不少。另外,這些中小專案,引入

dva/redux

這些工具,確實顯得偏重。透過函式式元件+

hoox

,即保證了輕量級,也滿足了全域性狀態管理的場景。

但是呢,它確實還有不少缺點。而且如果是真的想吃透 99%的場景,可能還需要補充一些配套工具。包括提到的

lint

外掛,甚至是類似

redux-devtools

這樣的工具。目前的我,還不敢發正式版,也不敢拿自己部門來背書。所以這篇文章,包括在掘金,我都沒有發到部門專欄。

不過如果你想用,基本還是可以放心的用。我們自己已經有多條業務在使用了,不是大版本,不可能 breaking change 了。只是說,它不一定是 React 狀態管理工具的終態……未來你們遇到更好的,還是可能會選擇遷移。

最後總結一下就是:問題不大,歡迎使用!

Github

具體的原始碼跟詳細 api 介紹可以見 github:

https://

github。com/wuomzfx/hoox

關於原始碼部分我就不詳細說明啦,也沒幾行程式碼,看看就能明白。