我剛開始學 React 的時候,差不多就是開始寫 JS 的時候,所以學得很吃力。還好當時看了 Zet 大大的《React 思維進化》,(某屆鐵人賽的系列文的實體書!真的推)在系列的尾聲詳細的介紹了 React 的機制,真的非常詳盡!那這篇文將會以我的理解再解釋一次,然後來實作一些 customize hook。
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 操作很昂貴),等到準備好後才一次批量更新。
而 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 外,我們也可以寫自己的 costumize hook 。
像是第一天中提到 hook 的陷阱,我們可以結合第一跟第二種解法,並使用 useRef
和 useCallback
寫一個自定義的 hook 去每次更新最新的 state。
我通常會用這個流程設計 Hook,特別是複雜功能(像資料抓取、表單、動畫等):
(1) 定義使用者 API(Consumer Experience First)
const { data, loading, error, refetch } = useFetch(url, { retry: 3 });
而不是一開始就先想「怎麼 fetch」。
(2) 輸入 / 輸出穩定性
deps
決定何時觸發,提供選項(options)做行為控制。useCallback
/ useMemo
),避免讓使用它的元件無謂 re-render。(3) 拆分職責(Single Responsibility)
useFetch
裡的 retry、cache 其實可以分成 useRetry
、useCache
再組合。(4) 狀態管理
useState
或 useReducer
規劃資料、loading、error 狀態。useReducer
比較安全。(5) 副作用控制
useEffect
vs useLayoutEffect
)。ref
避免 stale closure)。(6) 可擴展性
(7) 錯誤處理
(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
回呼使用範例:
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
包裝,確保在依賴(debounce
、onChange
)不變的情況下,不會重新生成,減少不必要的 re-render。在這個函數中,如果設置了防抖時間,會先清掉前一次的計時器,然後在延遲時間後再去更新 state,並且呼叫 onChange
;如果沒有防抖,則直接同步更新 state 並回呼 onChange
。
toggle
、setTrue
、setFalse
則是針對布林值操作的便捷方法,它們本質上都是呼叫 updateValue
,只是傳入的邏輯不同。最後的 useEffect
只在組件卸載時執行一次,用來清理尚未結束的防抖計時器,避免內存洩漏或對已卸載組件進行狀態更新。整體設計既封裝了布林狀態的常用操作,又支援狀態變更監聽與更新節流,讓它在 UI 交互(如開關按鈕、Modal 開關、動畫觸發)等場景中可以即插即用,並且保持良好的效能與安全性。
[Day 28] 一次弄懂 React hooks 的運作原理與設計思維(上) https://ithelp.ithome.com.tw/articles/10308283