當有多個組件有相同的邏輯,但卻重複寫了好幾次,這時候可以將相同邏輯的地方,抽出來做成一個共用 function,方便我們使用。
Custom Hook 會自然遵循 Hook 設計的規範,且所有內部的 state 和 effect 都是完全獨立的。
必須以 use
做開頭,方便開發者一眼就知道這是可以使用的 Hook,Lint 工具也會自動檢查是否違反 Hook 規則,如:
useDocumentTitle
useCounter
useInterval
以下會有兩個範例來做解說
目前有兩個 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
◆ 調整成 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>
);
}
這樣就打造好 useCounter 囉!要修改邏輯部份時,不用再去買個組件,直接從 useCounter.js 一次修改完成
現在有兩個計數器,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>
);
}
情境說明
我們可以看到,雖然兩個組件邏輯有點不太一樣,但都有使用到 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>
);
}
如此一來,組件可以使用 useInterval 傳入參數,未來如果也要新增 setInterval 的功能,就可以直接使用 useInterval
Custom Hook 提供了共享邏輯的靈活性,可讀性大幅的提高,也省去重複新增修改的麻煩,相信你已經知道怎麼使用 Custom Hook,打造一個屬於你的 Hook 吧!
Building Your Own Hooks
React Hooks 系列之8 custom Hook
使用 React Hooks 聲明 setInterval
本文將同步更新至我的部落格
Lala 的前端大補帖