React Hooks 介紹
React 引用的 Hooks 設計,讓副作用處理和外部功能更輕鬆地進入函式元件,相比於類別元件的寫法,提供了更簡單的解決方案。舉例來說,Redux 的 useSelector
可以取代 connect
和 mapStateToProps
。接下來,我們將通過 useState
和 useEffect
兩個 Hooks 的範例,來對比其與類別寫法的不同。
React 元件的兩種形式
React 元件有兩種形式:類別(Class)和函式(Function)。這兩種形式在狀態管理上有所不同:
- Class 元件:通過
this.setState()
來直接設定元件中的狀態。 - Function 元件:使用
const [state, setState] = useState()
來回傳的setState()
函數進行狀態更新。
1 | // 使用 connect 的寫法 |
useState 的概念與理解
useState 是一個讓函式元件擁有狀態的 Hook。它的運作方式類似於 JavaScript 的閉包模式,讓你可以在函式中管理狀態,而不是使用類別元件中的 this.state
。
useState 純值與物件比較
狀態可以是基本型別(Primitive)或物件型別(Object):
- Primitive type:通過值傳遞(by value)。
- Object type:通過引用傳遞(by reference)。
在 React 中,使用 useState
時的考量包括:
- Primitive:比較單純,適合新手使用。
- Object:對於多層物件結構,建議將物件進行正規化 (normalize)以簡化比較和更新。
useState(Primitive) vs useState(Object)
在使用 Hook 時,可以採用以下兩種策略來處理複雜狀態:
- useState(Primitive):宣告多個 useState,每個狀態變數單獨管理。
- useState(Object):使用一個 useState,將多個狀態存儲在一個物件中。
由於 class 的狀態一定是物件的型態,對於 Object 型態的狀態會有比較好的處理,舉例來說像是物件的合併機制。
1 | // class setState 實際上做的事情 |
相對於 hook 在設定上實際上沒有針對物件做預設的物件合併機制。
1 | // setHookState 其實就是單純設定 |
JavaScript 物件比較
由於物件型態是 by reference ,使用上會有需要考量相等的比較方式。
- 這個例子來說物件並不相等
{display: "flex"} === {display: "flex"} // false
在 JavaScrpit 中有三種比較方式
- strict equality operator ===
- loose equality operator ==
- Object.is()
在 React 中會用到的方式是 shallow object equality check
- Shallow Equality: 會迭代物件物件中的 keys 如果都是
===
就當他相同- React.PureComponent 的 shouldComponentUpdate 實作的判斷方式
物件型態是按引用比較的,即 {display: "flex"} === {display: "flex"} // false
。
React 使用淺層比較來判斷物件是否相同。
1 | function shallowEqual(object1, object2) { |
Module Pattern 與 useState 的比較
Module Pattern 使用 IIFE(Immediately Invoked Function Expression)來創建私有變數和公共函式,類似於 useState 提供的狀態管理方式。
1 | const moduleCounter = (function () { |
這與 useState 類似,都是利用閉包來保存狀態和操作函式。
1 | function Counter() { |
useEffect 的概念與理解
useEffect
用來處理副作用,相當於類別元件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
。它接受兩個參數:副作用函式和依賴陣列。
1 | // componentDidMount |
使用 useEffect 的實際案例
例如,從 API 取得資料:
1 | async componentDidMount() { |
由於 React 是 component-based 的一個函式庫,所以元件本身的定義和規範就蠻重要的,其中比較特殊的是元件在實際運用上會有一些生命週期,大致上我們平常會使用到的就是 componentDidMount
及 componentDidUpdate
,剩下可能會用到但比較少的是 componentWillUnmount
。
由於是寫在 function 中,所以可以想像整個 function 的內容都是原來寫法中 render()
裡的內容,差別在把 constructor
中的狀態用其他的方法寫在這個 function 裡面,元件原本由狀態改變來驅動的特性一樣沒有改變。
更好的寫法則是再抽出來,就會變下面這樣,更清楚也更好測試,也可以重複的去使用相關邏輯。
1 |
|
useEffect 的常見問題與解決方案
- 依賴缺失警告 (useEffect has a missing dependency):確保所有在 useEffect 中使用的變數都包含在依賴陣列中。
- 元件卸載後更新 (update on a unmounted component):在清理函式中處理 API 請求或計時器的中止。
1 | useEffect(() => { |
- useEffect 中去打 API,但是資料回來時使用者已經切換到其他畫面
1 | useEffect(() => { |
- timer function 也是同樣的概念
1 | useEffect(() => { |
- websocket 由於跟 API 的概念不太一樣,做法就是直接關掉
1 | useEffect(() => { |
hook 效能優化
在撰寫 hook 且狀態較複雜時,會有底下兩種策略
- useState(Primitive): 宣告多個 useState 搭配多個值
- useState(Object): 宣告一個 useState 搭配一個物件中含有多個值
在 React 18 版之前,宣告多個 State 的寫法如下:
1 | const [isLoading, setLoading] = useState(true); |
這樣的寫法會因為兩個 setState 發生在非同步完成之後,所以 React 並不會 batch 他們而造成兩次的畫面渲染。
所以在 React 還沒協助處理之前,最簡單的方式就是透過物件一次設定,或是使用 unstable_batchedUpdates
。
1 | setStateOnce({ |
有篇 Github 上的討論蠻精彩的,有興趣可以參考如下:
https://github.com/reactwg/react-18/discussions/21
內文中也有提供範例,大家可以進去試用看看兩者差異。
- ✅ Demo: React 17 batches inside event handlers. (Notice one render per click in the console.)
- 🟡 Demo: React 17 does NOT batch outside event handlers. (Notice two renders per click in the console.)
在剛開始寫 hook 的時候,可能會寫像是以下的程式碼。下面 console.log
會一直印,代表每次都重新 render,但 Increment 這個其實沒有改變,該怎麼避免?
1 | import { useState } from "react"; |
除了基本的 hook 以外,常用的還有以下:
- useState: 加入元件狀態,但這裡的 setState 不會幫我們自動合併物件型態的狀態,需要用 callback 方式寫並且自行合併
- useReducer: 可以用來處理物件型態的狀態
- useEffect: 處理 side effect,取代 componentDidMount, componentDidUpdate, componentWillUnmount
- useCallback: 當 function 需要在 useEffect 中被使用但又不想加入觸發條件
- useMemo: 把較高成本計算記起來
- useRef: 取得參考用的 object
剛剛解決方案的小提示
- useMemo: 可以記住函式運算值
- useCallBack: 可以記住函式
React 18 useState Automatic Batching
前面提到的問題將在 React 18 之後獲得解決,即使是非同步 callback 中的 setState 也會自動 batch 而不會造成兩次的渲染,還是想分開反而要自己使用 ReactDOM.flushSync
來分開。
1 | function App() { |
hooks 優點與缺點
優點
- 更接近原生 JavaScript,對於初學者友好,且不需要理解 ES6。
- 減少複雜的元件週期管理,只需控制
useEffect
。 - 提供簡化的解決方案,如 Redux 的
useSelector
。 - 更容易進行程式碼壓縮和最佳化。
缺點
useEffect
可能會將多個副作用合併,需謹慎管理依賴。- 避免在函式中使用
new
或未處理的事件監聽器,避免每次渲染時都重做一次。 - 尚未涵蓋
getSnapshotBeforeUpdate
和componentDidCatch
這兩個生命周期方法。
壓縮 (minified)
React 團隊指出,Hook 是純函式,這使得程式碼壓縮(minified)變得更簡單,相對於類別元件來說更易於進行壓縮和優化。
注意: 詳細閱讀 React Hooks 文件 以了解更多信息。
喜歡這篇文章,請幫忙拍拍手喔 🤣