iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
Modern Web

JavaScript 進階修煉與一些 React ——離開初階工程師新手村的頭30天系列 第 9

離開 JS 初階工程師新手村的 Day 09|自訂 Hook:鍛造自己的技能卷軸

  • 分享至 

  • xImage
  •  

我剛開始學 React 的時候,差不多就是開始寫 JS 的時候,所以學得很吃力。還好當時看了 Zet 大大的《React 思維進化》,(某屆鐵人賽的系列文的實體書!真的推)在系列的尾聲詳細的介紹了 React 的機制,真的非常詳盡!那這篇文將會以我的理解再解釋一次,然後來實作一些 customize hook。

React Hooks 的內部原理簡介

Fiber

Fiber 是 React 在管理 component 更新時所使用的內部資料結構與運作模型。在 Fiber 架構中,每個畫面上的元件會對應到一個 Fiber 節點,每個元件 render 時,React 都會為它建立(或更新)一個對應的 Fiber 節點,裡面記錄了這個元件的類型、props、state、Hooks 狀態鏈表、子元件關係,以及與前一次 render 的差異比對資訊。這些 Fiber 節點最終組成了一棵樹,代表了 React 在記憶體中維護的一個虛擬結構,它代表了目前應用程式的元件層級與 UI 狀態。

在更新時,React 會建立一棵對應的「work-in-progress Fiber 樹」作為新版本,並在完成比較與更新決策後,將這棵新樹替換為當前顯示的 Fiber 樹。這樣分開的好處是,React 可以在記憶體中先計算、比對、優化 Fiber 樹,不必每次 render 都直接碰 DOM(因為 DOM 操作很昂貴),等到準備好後才一次批量更新。

Hook list

而 React 在對應的 Fiber 節點中維護一條 Hook list,儲存每個 Hook 的狀態與依賴資訊。以 useState 為例,第一次 render 時 React 會建立一個包含初始值的 Hook 節點並回傳狀態與更新函式,之後每次 render 都會重跑整個 component 並按照呼叫順序,回到對應的 Hook 節點讀取最新的狀態。而呼叫 setState 並不會立即改變變數,而是排程一次新的 render,等到下一次 render 時再透過 Hook 節點拿到更新後的值,所以不能在條件判斷中呼叫 hook,否則 hook 節點對應會錯位,導致資料對不上。

useEffect 則是專門用來處理副作用的 Hook,與 useLayoutEffect 的差別在於它的執行時機。React 在完成整個 commit 階段、畫面繪製之後,才會非同步地執行 useEffect 的回呼,確保不會阻塞 UI 更新。而 useLayoutEffect 會在 DOM 變更寫入後、瀏覽器繪製前同步執行,適合需要即時讀取或修改 DOM 的場景,例如量測元素大小、同步捲動位置等。每一次 effect 在重新執行前,React 都會先呼叫前一次的清理函式(cleanup),確保舊的訂閱、計時器或事件監聽被移除,避免資源洩漏或狀態混亂。

React Hook 的核心概念是:

  • useRef 是持久化的容器

    它會在多次 render 間保持同一個物件引用,不會因為 re-render 重設。

  • useContext 是直接讀取當前 Fiber 樹的 Context 快照,而不是像 props 那樣層層傳遞。


自定義 Hook 範例

除了以上提到的常用、通用 Hook 外,我們也可以寫自己的 costumize hook 。

像是第一天中提到 hook 的陷阱,我們可以結合第一跟第二種解法,並使用 useRefuseCallback 寫一個自定義的 hook 去每次更新最新的 state。

自定義 Hook 設計思路

我通常會用這個流程設計 Hook,特別是複雜功能(像資料抓取、表單、動畫等):

(1) 定義使用者 API(Consumer Experience First)

  • 問自己如果我是用這個 Hook 的人,我希望怎麼用?」
  • 優先設計回傳值的格式與命名,再去思考內部怎麼實現。
  • 範例:一個資料抓取 Hook
const { data, loading, error, refetch } = useFetch(url, { retry: 3 });

而不是一開始就先想「怎麼 fetch」。

(2) 輸入 / 輸出穩定性

  • 輸入:用 deps 決定何時觸發,提供選項(options)做行為控制。
  • 輸出:回傳的物件或方法,要有穩定的引用(用 useCallback / useMemo),避免讓使用它的元件無謂 re-render。

(3) 拆分職責(Single Responsibility)

  • 一個 Hook 只做一件事,可以再被組合成更高階 Hook。
  • 例:useFetch 裡的 retry、cache 其實可以分成 useRetryuseCache 再組合。

(4) 狀態管理

  • 使用 useStateuseReducer 規劃資料、loading、error 狀態。
  • 複雜狀態更新(特別是多個相關值同時改變)→ 用 useReducer 比較安全。

(5) 副作用控制

  • 明確規劃哪些副作用要在何時執行(useEffect vs useLayoutEffect)。
  • 確保有 cleanup(取消請求、移除事件監聽、停止動畫等)。
  • 小心依賴陣列(可能需要用 ref 避免 stale closure)。

(6) 可擴展性

  • 是否要支援額外功能(debounce、throttle、cache、重試)。
  • 預留參數位置或 option,而不是一次塞滿功能。

(7) 錯誤處理

  • 要不要把錯誤 throw 出去(讓 ErrorBoundary 處理)還是回傳 error 物件。
  • API 呼叫失敗要不要重試或快取。

(8) 可觀察性(debug-friendly)

  • 內部可以加 console.debug(在 dev 模式下),或透過事件回呼(onChange / onError)讓外部知道狀態變化。
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    console.log(count); // 期望點擊時顯示最新 count,實際不是
    setCount(count + 1);
  };

  useEffect(() => {
    const id = setInterval(handleClick, 1000);
    return () => clearInterval(id);
  }, []);

  return <div>Count: {count}</div>;
}

變成

function useLatest(fn) {
  const ref = useRef(fn);
  useEffect(() => {
    ref.current = fn;
  });
  return useCallback((...args) => ref.current(...args), []);
}

我們將把一個最常見的布林狀態管理模式(開/關)包裝成一個可重複使用的工具,同時在內部加入了狀態**變更回呼(onChange)可選防抖(debounce)**的功能,讓它不只是一個單純的 useState 封裝,而是可以在 UI 與業務邏輯之間多做一層可控的狀態流管理。除了單純切換 true/false,還支援:

  • toggle():切換狀態
  • setTrue()setFalse():直接設置
  • onChange 回呼
  • 狀態變化防抖(防止太頻繁切換)

https://ithelp.ithome.com.tw/upload/images/20250919/201683656SDbb47Wn6.png

使用範例:

function ToggleButton() {
  const { value, toggle, setTrue, setFalse } = useBool(false, {
    onChange: (v) => console.log("New Value:", v),
    debounce: 300,
  });

  return (
    <div>
      <p>狀態:{value ? "ON" : "OFF"}</p>
      <button onClick={toggle}>切換</button>
      <button onClick={setTrue}>設為 ON</button>
      <button onClick={setFalse}>設為 OFF</button>
    </div>
  );
}

首先,核心狀態是透過 useState(initial) 建立的,這和一般 React 狀態管理一樣,保證每次組件重新 render 時,都能從 Fiber 樹的 hook 狀態鏈表中取回對應的布林值。useRef 則用來保存防抖計時器的引用,因為它在 render 間不會改變,適合儲存這類跨 render 的 side effect 資源。updateValue 是整個 hook 的關鍵,它使用 useCallback 包裝,確保在依賴(debounceonChange)不變的情況下,不會重新生成,減少不必要的 re-render。在這個函數中,如果設置了防抖時間,會先清掉前一次的計時器,然後在延遲時間後再去更新 state,並且呼叫 onChange;如果沒有防抖,則直接同步更新 state 並回呼 onChange

togglesetTruesetFalse 則是針對布林值操作的便捷方法,它們本質上都是呼叫 updateValue,只是傳入的邏輯不同。最後的 useEffect 只在組件卸載時執行一次,用來清理尚未結束的防抖計時器,避免內存洩漏或對已卸載組件進行狀態更新。整體設計既封裝了布林狀態的常用操作,又支援狀態變更監聽與更新節流,讓它在 UI 交互(如開關按鈕、Modal 開關、動畫觸發)等場景中可以即插即用,並且保持良好的效能與安全性。


References

[Day 28] 一次弄懂 React hooks 的運作原理與設計思維(上) https://ithelp.ithome.com.tw/articles/10308283


上一篇
離開 JS 初階工程師新手村的 Day 08|從新手短劍到魔法武器:ES6+ 重構實戰
下一篇
離開 JS 初階工程師新手村的 Day 10|強大組合技:HOC 與 props 傳遞
系列文
JavaScript 進階修煉與一些 React ——離開初階工程師新手村的頭30天10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言