HooX: 應該不是 React 下一代狀態管理工具
前言
其實在一個多月前,我也已經在掘金髮過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
關於原始碼部分我就不詳細說明啦,也沒幾行程式碼,看看就能明白。