Sean's Blog

An image showing avatar

Hi, I'm Sean

這裡記錄我學習網站開發的筆記
歡迎交流 (ゝ∀・)b

LinkedInGitHub

React Code Reuse - Custom Hooks

本文介紹為什麼我們要建立並使用 Custom Hooks,以及講解如何撰寫創建自己的 Hooks,讓我們在開發 React 專案時更好地複用各種邏輯與程式碼。

什麼是 Custom Hook

  • Custom Hooks 可以使用 React Hooks 與 State 等函式
  • 使用時機:當不同元件裡有著一定程度的共通邏輯時,我們會想要複用邏輯,在各個元件中只去撰寫不同的部分
  • 特性:每一次使用 Custom Hook 時,各別內部的 State 與 Effect 都是完全獨立的

示範建立一個 Custom Hook

Custom Hooks 其實不是 Functional Components,它是一個函式,只是在做法上有點類似

首先我通常習慣在 src 底下建立一個名為 hooks 的資料夾,專門用來存放自己建立的 Custom Hooks。而 Custom Hooks 的檔案名稱也是依照個人喜好即可,我自己喜歡用 useXxx.js 作為命名規則,舉例來說,新增 src/hooks/useCounter.js 檔案。

創建好檔案後,我們必須在將函式名稱命名為 useXxx,例如:useCounter。這種函式名稱的命名方式是必須遵守的規範,這是為了讓 React 能夠辨別這是一個 Custom Hook,讓你能夠在裡面使用 useEffect 等 Hooks。

1import { useEffect, useState } from 'react';
2
3const useCounter = () => {
4  const [counter, setCounter] = useState(0);
5
6  useEffect(() => {
7    const interval = setInterval(() => {
8      setCounter((prevCounter) => prevCounter + 1);
9    }, 1000);
10
11    return () => clearInterval(interval);
12  }, []);
13
14  return counter;
15};
16
17export default useCounter;
18

如同內建的 React Hooks,這個 Custom Hook 也會 return 東西,我們建立的 Custom Hook 可以回傳「任何」型別。在這個範例當中,我是回傳一個計算過後的 number

最後記得導出這個 Custom Hook,這樣我們才能在其他元件中調用它。

使用自己建立的 Custom Hook

現在我們就到其他元件中使用 useCounter,以此取得回傳值(到這裡,我們就已經成功做到邏輯拆分囉)。

在使用 Custom Hooks 的時候,如果在「多個元件」中使用同一個 Custom Hook,每個元件都會產生一套自己的 Custom Hook,也就是裡面使用的 State 或 Effect 等資料都是「不會共用」的。

共用的是邏輯,不會共用狀態

1const ForwardCounter = () => {
2  const counter = useCounter(); // 這個 counter 是 Custom Hook 回傳的
3
4  return <Card>{counter}</Card>;
5};
6
7export default ForwardCounter;
8

接下來,我們可以再針對不同的邏輯去做改變,像是透過「參數」來指定不同的邏輯。例如:透過 forwards 參數,給予 Custom Hook false 表示遞減,預設的 true 則為遞增。

1const useCounter = (forwards = true) => {
2  const [counter, setCounter] = useState(0);
3
4  useEffect(() => {
5    const interval = setInterval(() => {
6      // 根據參數的值判斷要執行的動作
7      if (forwards) {
8        setCounter((prevCounter) => prevCounter + 1);
9      } else {
10        setCounter((prevCounter) => prevCounter - 1);
11      }
12    }, 1000);
13
14    return () => clearInterval(interval);
15  }, [forwards]); // 記得把參數放入 Dependencies array
16
17  return counter;
18};
19

使用上加上參數,例如:選擇傳入 false 表示要執行遞減。

1const BackwardCounter = () => {
2  const counter = useCounter(false);
3
4  return <Card>{counter}</Card>;
5};
6

使用 Custom Hooks 的注意事項

1. 小心傳遞參考類型

如果 Custom Hook 回傳的內容包含函式(或物件),在使用 useEffect 時「不要」將它們加入 dependencies array 來偵測變化。

為什麼?理論上應該加入沒錯,但實際上這會導致無限循環!因為每次生成的新函式或物件雖然看起來一樣,但實際上是不同的引用,這會導致 useEffect 不斷重跑。

解決方法:對可能變動的函式(或物件)使用 useCallback(或 useMemo),這樣就能確保它們是同一個引用。

1import { useState, useEffect, useCallback } from 'react';
2
3const useCustomHook = () => {
4  const [value, setValue] = useState(0);
5
6  const updateValue = useCallback(() => {
7    setValue((prev) => prev + 1);
8  }, []);
9
10  return { value, updateValue };
11};
12
13const MyComponent = () => {
14  const { value, updateValue } = useCustomHook();
15
16  useEffect(() => {
17    updateValue(); // 加上 useCallback 避免導致無限循環
18  }, [updateValue]);
19
20  return <div>{value}</div>;
21};
22

2. 避免在使用 Custom Hook 時傳入參數,將外部依賴改為函式參數

除了使用 useCallbackuseMemo 之外,還有另一個解決方法,就是將 Custom Hook 所依賴的變數改為函式的參數。

這意味著在 Custom Hook 內部使用這些變數的地方,我們直接把它們作為函式參數來傳遞。這樣一來,我們就不需要在 Custom Hook 中傳遞這些變數,Custom Hook 也不需要新增 dependencies。

1import { useEffect } from 'react';
2
3const useCustomHook = () => {
4  const logDependency = (dependency) => {
5    console.log(dependency);
6  };
7
8  return logDependency;
9};
10
11const MyComponent = ({ someProp }) => {
12  const logDependency = useCustomHook();
13
14  useEffect(() => {
15    // 傳遞 someProp 作為參數給 logDependency,而不是直接依賴 someProp
16    // 減少了不必要的重複渲染和潛在的錯誤
17    logDependency(someProp);
18  }, [someProp, logDependency]);
19
20  return <div>{someProp}</div>;
21};
22

回顧

看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到…

  • Custom Hooks

References