iT邦幫忙

2022 iThome 鐵人賽

DAY 21
0
Modern Web

你 React 了嗎? 30 天解鎖 React 技能系列 第 21

[DAY 21] Custom Hook - 客製你自己的 Hook

  • 分享至 

  • xImage
  •  

cover

認識 Custom Hook

當有多個組件有相同的邏輯,但卻重複寫了好幾次,這時候可以將相同邏輯的地方,抽出來做成一個共用 function,方便我們使用。

Custom Hook 會自然遵循 Hook 設計的規範,且所有內部的 state 和 effect 都是完全獨立的。

Custom Hook 規則

必須以 use 做開頭,方便開發者一眼就知道這是可以使用的 Hook,Lint 工具也會自動檢查是否違反 Hook 規則,如:

useDocumentTitle
useCounter
useInterval

Custom Hook 範例

以下會有兩個範例來做解說

  • useCounter
  • useInterval

範例一:useCounter

目前有兩個 Counter 的組件,分別是 Counter1.js、Counter2.js,裡面有增加、減少跟重置的按鈕

components/Counter1.js

import { useState } from "react";

const Counter1 = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount((prevCount) => prevCount + 1);
  };
  const decrement = () => {
    setCount((prevCount) => prevCount - 1);
  };
  const reset = () => {
    setCount(0);
  };

  return (
    <div>
      <h2>Counter1:{count}</h2>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
      <button onClick={reset}>reset</button>
    </div>
  );
};

export default Counter1;

components/Counter2.js

import { useState } from "react";

const Counter2 = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount((prevCount) => prevCount + 1);
  };
  const decrement = () => {
    setCount((prevCount) => prevCount - 1);
  };
  const reset = () => {
    setCount(0);
  };

  return (
    <div>
      <h2>Counter2:{count}</h2>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
      <button onClick={reset}>reset</button>
    </div>
  );
};

export default Counter2;

兩個組件載入到 App.js
App.js

import Counter1 from "./components/Counter1";
import Counter2 from "./components/Counter2";

export default function App() {
  return (
    <div>
      <Counter1 />
      <Counter2 />
    </div>
  );
}

custom hook

codesandbox 程式碼範例

可以看到兩個組件的功能相同,但卻寫了兩次,我們可以把相同邏輯的地方抽出來變 custom hook



調整成 Custom Hook

在 hooks 資料夾新增 useCounter.js

hooks/useCounter.js

import { useState } from "react";

export const useCounter = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount((prevCount) => prevCount + 1);
  };

  const decrement = () => {
    setCount((prevCount) => prevCount - 1);
  };

  const reset = () => {
    setCount(0);
  };

  return [count, increment, decrement, reset];
};

把 Counter1、Counter2 相同邏輯的地方抽出來,並 return 變數方法給 useCounter

components/Counter1.js

import { useCounter } from "../hooks/useCounter";

const Counter1 = () => {
  const [count, increment, decrement, reset] = useCounter();

  return (
    <div>
      <h2>Counter1:{count}</h2>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
      <button onClick={reset}>reset</button>
    </div>
  );
};

export default Counter1;

components/Counter2.js

import { useCounter } from "../hooks/useCounter";

const Counter2 = () => {
  const [count, increment, decrement, reset] = useCounter();

  return (
    <div>
      <h2>Counter2:{count}</h2>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
      <button onClick={reset}>reset</button>
    </div>
  );
};

export default Counter2;

App.js

import Counter1 from "./components/Counter1";
import Counter2 from "./components/Counter2";

export default function App() {
  return (
    <div>
      <Counter1 />
      <Counter2 />
    </div>
  );
}

codesandbox 程式碼範例

這樣就打造好 useCounter 囉!要修改邏輯部份時,不用再去買個組件,直接從 useCounter.js 一次修改完成


範例二:useInterval

現在有兩個計數器,Counter 為正向計數,BackCounter 為反向計數,並載入在 App.js

components/Counter.js

import { useEffect, useState } from "react";

const Counter1 = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      if (count < 100) {
        setCount((count) => count + 1);
      }
    }, 100);

    return () => {
      clearInterval(timer);
    };
  }, [count]);

  return <h1>Counter1:{count}</h1>;
};

export default Counter1;

components/BackCounter.js

import { useEffect, useState } from "react";

const BackCounter = () => {
  const [count, setCount] = useState(100);

  useEffect(() => {
    const timer = setInterval(() => {
      if (count > 0) {
        setCount((count) => count - 1);
      }
    }, 100);

    return () => {
      clearInterval(timer);
    };
  }, [count]);

  return <h1>BackCounter:{count}</h1>;
};

export default BackCounter;

App.js

import Counter from "./components/Counter";
import BackCounter from "./components/BackCounter";

export default function App() {
  return (
    <div className="App">
      <Counter />
      <BackCounter />
    </div>
  );
}

情境說明

  • 正向計數:從 0 開始每秒 +1,如數字到 100 將會停止
  • 反向計數:從 100 開始每秒 -1,如數字到 0 將會停止

custom hook

codesandbox 程式碼範例

我們可以看到,雖然兩個組件邏輯有點不太一樣,但都有使用到 setInterval 的部分,跟 useEffect 的清除函式,這時候可以把 setInterval 抽出來,變成 useInterval


調整成 Custom Hook

hooks/useInterval.js

import { useEffect, useRef } from "react";

export const useInterval = (callback, delay) => {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    if (delay !== null) {
      const timer = setInterval(tick, delay);

      return () => clearInterval(timer);
    }
  }, [delay]);
};

將 callback function 和秒數變成參數帶入,並用 useRef、useEffect 確保回傳最新的 function

components/Counter.js

import { useState } from "react";
import { useInterval } from "../hooks/useInterval";

const Counter1 = () => {
  const [count, setCount] = useState(0);

  useInterval(() => {
    if (count < 100) {
      setCount((count) => count + 1);
    }
  }, 1000);

  return <h1>Counter1:{count}</h1>;
};

export default Counter1;

components/BackCounter.js

import { useState } from "react";
import { useInterval } from "../hooks/useInterval";

const BackCounter = () => {
  const [count, setCount] = useState(100);

  useInterval(() => {
    if (count > 0) {
      setCount((count) => count - 1);
    }
  }, 1000);

  return <h1>backCounter:{count}</h1>;
};

export default BackCounter;

App.js

import Counter from "./components/Counter";
import BackCounter from "./components/BackCounter";

export default function App() {
  return (
    <div className="App">
      <Counter />
      <BackCounter />
    </div>
  );
}

codesandbox 程式碼範例

如此一來,組件可以使用 useInterval 傳入參數,未來如果也要新增 setInterval 的功能,就可以直接使用 useInterval


結語

Custom Hook 提供了共享邏輯的靈活性,可讀性大幅的提高,也省去重複新增修改的麻煩,相信你已經知道怎麼使用 Custom Hook,打造一個屬於你的 Hook 吧!/images/emoticon/emoticon12.gif


Reference

Building Your Own Hooks
React Hooks 系列之8 custom Hook
使用 React Hooks 聲明 setInterval


本文將同步更新至我的部落格
Lala 的前端大補帖



上一篇
[DAY 20] useRef 儲存資料與指定 DOM 元素
下一篇
[DAY 22] useContext 跨組件溝通傳遞資料
系列文
你 React 了嗎? 30 天解鎖 React 技能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言