用八二法則寫 custom react-hooks
根據八二法則,只需要兩成的功就能滿足八成的事,那我們來用兩成的 built-in hooks 來封裝八成我們會用到的 custom hooks 吧!
寫在文前
這不是一篇正經的 react-hooks 知識點文章,更多的反而是我的 murmur 跟一些對於 hooks 怎麼從無到有的一點思考路線。而下半段會做 code-reviewing。
內建的 hooks
這麼多 hooks ,分別:
- 控 state 的 (
useState
、useReducer
、useContext
) - 控 Effect 的(
useEffect
、useLayoutEffect
) - 控 ref 的(
useRef
、useImperativeHandle
) - 優化用的(
useMemo
、useCallback
) - Debug 用(
useDebugValue
, 我沒用過)
由於八二法則的原因,所以我們就從十個 hooks 挑兩個來用吧!
useState & useEffect,必要時我們再挑其他的進來打組合拳。
useState & useEffect 在幹嘛?
一個控 state ,state 變了組件就會更新,更新就會有 effect 觸發,最後組件 unmount 再把 effect 全部清掉。這就是一個簡單的函數組件會發生的小事。
L4: 設定初值並回傳 state 的 getter setter
L6-L7: 在這邊寫下狀態 update 之後可能會造成環境髒亂的 Effect code。(如attach event、修改樣式)
L8-L12: 組件unmount 幫 L6-L7 造成的 Effect 擦屁股。(如detach event)
會這兩個 hook 還能幹嘛?
好,看完以上文檔 -like 的廢話,我們開始切入正題,到底如何八二法則的寫一個自己的 custom hook。
讓我們用 react-use 這個 react hooks opensource 專案來舉例子吧!
- 優雅地拿外部(window)滾動狀態——useWindowScroll
- 優雅地拿外部(window)的 size ——useWindowSize
- 優雅地拿外部(裝置)電量狀態——useBattery
那我們開始前,由於你知我知獨眼龍也知, code 總是會有更動,加上這篇文章主要是 reviewing 性質,我們依照發文當下的 HEAD 90e72a5
,而你們點進去的連結會直接切近這個 commit hash 。
useWindowSroll
L11-L15 (useState): 設定 state
L17-L46 (useEffect):實際上在做的只要兩件事
- (effect) attach event
- (cleanup) detach event
這是一個 event relative 的 custom hook 的一個常見 Flow。
注意,他們的 comparedArray 是[]
,意味著只有一開始跟最後會觸發 useEffect,也就是說 mounted->attach v.s unmounted->detach。
最後 event callback 做了什麼呢?就是在 scroll event trigger 的時候把新的 pageXOffset
/ pageYOffset
寫進 state。
就是這麼簡單
useWindowSize
[]
,所以只會在 mounted 時 attach , unmounted detach event。L7–10(useState):我們會用到寬高
L12–30(useEffect):其實跟偵測 scroll 狀況大同小異
- (effect) attach window event,
resize
。 - (cleanup) 把上述 event detach 掉。
useBattery
[]
,所以只會在 mounted 時 attach , unmounted detach event。L36 (useState):isSupported
& fetched
L38-L76 (useEffect):雖說是 device info ,但實際上也是透過 browser 提供的 event 去偵測電量變化。
- (effect) attach events,包括
chargingchange
,chargintimechange
,dischargingtimechange
,levelchange
- (cleanup) 把上述四個 event detach 掉。
至於這四個 event 究竟做了什麼,可以參考這頁 MDN 的文檔。
(不過你可以看到 MDN 不太推薦你使用就是了。)
你會發現,啊乾,怎麼三個長的差不多? (不然我怎麼會說八二法則)
他們都是在藉由各物件(像 window 、 document 、 batteryManager)的 event 來實時地 monitor 外部的 value 。
- 註冊 event 到 react element (
onClick
),react 在 lifecycle 結束後會幫你清除掉。 - 註冊在外部的(
addEventListener
),如果你沒有手動清除,它就會一直掛在那,這種就是 side-effect 。
所以 cleanup function 就是個擦屁股的角色 lol。
useRef
如果你會寫 class component,你一定曾經幹這種事 this.count = 20
,整個class component 因為 share 這個 this, 所以都可以 access 到,且不會驚動到 re-render。
如果你不常寫 class component,那麼很多時候 temp value 是不需要觸發組件更新的,比如跟組件更新無關的變數 、 element ,這些跟組件更新沒啥關係的東西。
是的,useRef 就是幫你完成這件事情的,雖然函數組件可以直接宣告變數,但每次 render 又會再重新 declare 一次,see this。
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
第一次走完之後 inputEl.current
就會指向這個 input element。
繼續我們的 code reviewing 之旅吧
- 優雅地改 document title —— useTitle
- 優雅地將 throttle 密集變動的 props ——useThrottle
useTitle
我寫到這,發現現在的版本有 bug (commit hash 90e72a5
) ,我發 PR 過去修了,先 skip。
useThrottle
useThrottle 要 review 之前,要先來理解一下 document:
簡單來說,useThrottle 的目的在於將密集變動的 props throttle 的方式變成 state 。
我們先不考量這樣的利弊去做 Review(我個人會傾向在第一個產生 props.value 那層,就先 throttle <Demo value={throttleValue}/>
,而非 throttle props)。
L6–8 我們把 timer id 、 nextValue 、 hasNextValue 存在 useRef 裡面,他們跟 timer 是否重新設定、在裡頭的值有關,但這些跟組件的 state / rendering 無關。
整個 custom hook 的 flow 是這樣:
value
change,設定一個計時器幫我更新(也許200ms)- 若 1. 還沒執行 timer callback,有
value
的 update ,則進入 L23-25,將其設為下一個被設定的nextValue
- 若 1. 2. 還沒執行,有新 value update,則進入 L23–25 ,
nextValue
被 override 為最新值(意味著 2. 被 throttle 掉囉)
在這個例子中,以 nextValue
、 hasNextValue
這些變數而言,沒有排到隊根本就沒有進到 state 造成額外 re-render 成本上升的必要。
尾聲
如何?在這樣的旅程裡,你透過兩成的 hooks 就能做簡單的 code reviewing,挺有成就感的吧!
我記得 hooks 剛出來的時候,有一些人覺得 hook 可以讓 react 做很多事,但其實沒有哇,你可以看到那些很特別的功能(比如 battery),跟 hook 本身沒有關係。
封裝 Hook 的代碼行數比起 class component 特別拆週期去實作較短、明確,但其實常常會犯錯,官方的 lint rule 是一個很好的輔助工具。也可以透過 React 嚴格模式,來測試其可用性與穩定性。