用八二法則寫 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 專案來舉例子吧!

  1. 優雅地拿外部(window)滾動狀態——useWindowScroll
  2. 優雅地拿外部(window)的 size ——useWindowSize
  3. 優雅地拿外部(裝置)電量狀態——useBattery

那我們開始前,由於你知我知獨眼龍也知, code 總是會有更動,加上這篇文章主要是 reviewing 性質,我們依照發文當下的 HEAD 90e72a5 ,而你們點進去的連結會直接切近這個 commit hash 。

useWindowSroll

L11-L15 (useState): 設定 state

L17-L46 (useEffect):實際上在做的只要兩件事

  1. (effect) attach event
  2. (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

L7–10(useState):我們會用到寬高

L12–30(useEffect):其實跟偵測 scroll 狀況大同小異

  1. (effect) attach window event, resize
  2. (cleanup) 把上述 event detach 掉。

useBattery

L36 (useState):isSupported & fetched

L38-L76 (useEffect):雖說是 device info ,但實際上也是透過 browser 提供的 event 去偵測電量變化。

  1. (effect) attach events,包括 chargingchange, chargintimechange, dischargingtimechange, levelchange
  2. (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

React 文檔的例子

第一次走完之後 inputEl.current 就會指向這個 input element。

繼續我們的 code reviewing 之旅吧

  1. 優雅地改 document title —— useTitle
  2. 優雅地將 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 是這樣:

  1. value change,設定一個計時器幫我更新(也許200ms)
  2. 若 1. 還沒執行 timer callback,有 value 的 update ,則進入 L23-25,將其設為下一個被設定的 nextValue
  3. 若 1. 2. 還沒執行,有新 value update,則進入 L23–25 ,nextValue 被 override 為最新值(意味著 2. 被 throttle 掉囉)

在這個例子中,以 nextValuehasNextValue 這些變數而言,沒有排到隊根本就沒有進到 state 造成額外 re-render 成本上升的必要。

尾聲

如何?在這樣的旅程裡,你透過兩成的 hooks 就能做簡單的 code reviewing,挺有成就感的吧!

我記得 hooks 剛出來的時候,有一些人覺得 hook 可以讓 react 做很多事,但其實沒有哇,你可以看到那些很特別的功能(比如 battery),跟 hook 本身沒有關係。

封裝 Hook 的代碼行數比起 class component 特別拆週期去實作較短、明確,但其實常常會犯錯,官方的 lint rule 是一個很好的輔助工具。也可以透過 React 嚴格模式,來測試其可用性與穩定性。

If any interest, 👉 https://realdennis.me.

If any interest, 👉 https://realdennis.me.