React Hooks API ⎯ 不只是 useState 或 useEffect

TL;DR: 你可能低估了官方 hook API 的功能

React 16.8 推出之後,想必大家都很關注這個鎮上新來的酷小孩,但是大部分的教學與範例似乎都圍繞在 useStateuseEffect 兩個 hook 之上,然而其實 React 官方提供的 API 還有不少有趣的 hook 可以利用,說不定將官方文件好好的讀過一遍之後,會發現你並不需要引入新的套件,用現有的 hook API 就能解決問題。

這篇文章主要是要介紹一些比較少被提及,但其實相當好用的官方提供的 Hook API ,以及對比較常用的 useEffect hook 的補充。


useCallback/useMemo

在預設的 React.Component 的機制裡,如果 props/state 發生改變即會觸發 重新渲染(re-render) ,而 parent 重新渲染時理所當然的會重新渲染 child components 。

為了達到優化的效果我們可以透過 shouldComponentUpdate 這個 lifecycle 方法來選定只有特定的 props 有改變時才更新 component ,或是改使用 React.PureComponent ,這樣就能僅在任何 props/state 有改變時才觸發更新。

更多關於 PureComponent 優化的說明可以參考下面這篇文章:

而在 React 16.6 引入的 React.memo 讓 function component 也可以獲得 PureComponent 的效能補強。

但是, shouldComponentUpdate 比較 props/state 是否相等的基準是使用shallowEqual ,特性是會以 object 的第一層 key 做 strict equal (===),換言之即使是同樣的值,若 reference 的物件不同就會被視為是不同的物件。

考慮以下程式碼:

可以在這裡看實際運行結果: https://codepen.io/yanglin_demo/pen/XOwjpZ

看似沒有問題, ChildComponent 用 React.memo 包起來,在 setState 裡也沒有對 ChildComponent 的 props 做改動,理論上運行這段程式碼時我們會期待看到 clicked! 3 次,再按 ChildComponent 按鈕時應不會觸發第 3 行的 console.log。

然而實際運行時會發現:當每個 ChildComponent 的按鈕點擊時,仍然會觸發 console.log。原因就在於 ParentComponent 的 onClick 在 ParentComponent 自身重新渲染時被重新綁定到一個新的箭頭函數,在 shallowEqual 的 strict equal 的情況下會被視為是不同的物件從而造成 memo 失敗。

在 class component 中,我們可以將這樣作為 props 傳遞下去的 callback 綁定到 class 上變成 member function ,但是 function component 不存在 instance,也就不能將 callback 綁定 ⎯ 這就是 useCallback/useMemo 的使用時機:在 function component 中產生一個不隨重新渲染而改動(mutate)的 callback。

我們將上列程式碼做一些小改動:

可以在這裡看實際運行結果: https://codepen.io/yanglin_demo/pen/gqNbjb

onClick useCallback 包起來,點擊 ChildComponent 的按鈕時就不會觸發 console.log 了! 💯 💯 💯

這讓 function component 可以做到一些以往只有 class component 能做到的優化方式,而 useMemo 跟 useCallback 的使用方式類似,可以看看官方文件怎麼說:

useCallback(fn, inputs) is equivalent to useMemo(() => fn, inputs)

useLayoutEffect

如第一段所說,大部分的教學都著重在 useEffectuseState ,然而 useEffect 其實還有一個好兄弟 useLayoutEffect ,差別在於 useEffect 是非同步執行(不阻擋瀏覽器渲染),而 useLayoutEffect 是同步執行(等待瀏覽器渲染完成)。

簡而言之,useEffect 會在瀏覽器完整跑完 reflow/repaint 流程之後才觸發,適合放不與 DOM 相依、不會阻擋瀏覽器主線程渲染的 effect,如 data fetching、event handler binding 等等。

但是 useEffect 發生在瀏覽器 reflow/repaint 之後,如果某些 effect 是從 DOM 獲得值 (clientHeight、clientWidth) 並對 state 做操作的行為,就可以在 reflow/repaint 之前先做 state 的操作,有機會避免瀏覽器花大量成本重新跑 reflow/repaint 的完整循環。

關於瀏覽器的完整重新渲染流程可以參考這篇文章

需要注意的是,useLayoutEffect 的行為可能甚至跟現有的 componentDidMount/componentDidUpdate 比較接近,比起 useEffect

這個連結是一個解釋的很好的範例。


useRef

以往 ref 只被用來當作儲存 DOM 節點的變數,在 React Hooks API 裏拓展了 ref 的用法與定義,現在 ref 可以用來儲存任何會變動(mutable)的值,角色變得跟 class component 的 member variable 很像,解決了 function component 沒有 instance 不能儲存變數的問題。

這讓 ref 變得更加彈性,事實上官方文件就推薦使用 ref 來紀錄 prevProps/prevState,即可實作類似 class component 的其他 lifecycle 功能。

官方文件的解決方法

總結

Hooks 解決了長久以來 function component 比起 class component 缺的一些拼圖,包含優化手段、彈性、lifecycle 等等,也順帶解決了以往兩大 pattern (render props & HOC) 的一些缺點,並不是只是讓 function component可以用 state 和 componentDidUpdate 這麼簡單(當然這也是非常重要的特色)。

雖然 Hooks API 還在很早期階段,可能近未來還有許多 breaking change ,或許現在還不是最佳時機將這些 API 採用到專案中,但是我們可以從這些概念上學習到很多東西,看的見 React 官方展現的企圖心,非常值得期待。