React Hooks (8) 自訂 hook

建立 hook 函式

自訂 hook 的第一步是建立 hook 函式,下面是追蹤滑鼠座標的一個 hook,函式會將值回傳出來

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const useMousePosition = () => {
const [mouse, setMouse] = useState({});

useEffect(() => {
const getMouseEvent = (e) => {
setMouse({
x: e.clientX,
y: e.clientY,
})
}

window.addEventListener('mousemove', getMouseEvent);
return () => window.removeEventListener('mousemove', getMouseEvent)
}, []);

return mouse;
}

元件內引入 hook

1
2
3
4
5
6
7
8
9
10
11
const App = () => {
const [text, setText] = useState('');
const mouse = useMousePosition();

return (<>
{ mouse.x} { mouse.y }
<input type="text" onChange={e => {
setText(e.target.value)
}} />
</>)
}

在元件內即可使用自訂 hook

自訂 hook 命名

自訂 hook 必須以 use 開頭命名,但如果 function 裡面沒有用到 hook 的話,不用 use開頭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 🔴 Avoid: A Hook that doesn't use Hooks
function useSorted(items) {
return items.slice().sort();
}

// ✅ Good: A regular function that doesn't use Hooks
function getSorted(items) {
return items.slice().sort();
}

// ✅ Good: A Hook that uses other Hooks
function useAuth() {
return useContext(Auth);
}

useAxios hook

前端專案常需要打 api 取回遠端資料,我們可以將發出網路請求的過程包成一個 custom hook 方便重複使用。這邊會以axios套件為範例。

  • useAxios hook 範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// useAxios.jsx
import { useState, useEffect } from 'react';
import axios from 'axios';

axios.defaults.baseURL = 'https://jsonplaceholder.typicode.com';

/**
fixed :
- no need to JSON.stringify to then immediatly do a JSON.parse
- don't use export defaults, because default imports are hard to search for
- axios already support generic request in one parameter, no need to call specialized ones
**/
export const useAxios = (axiosParams) => {
const [response, setResponse] = useState(undefined);
const [error, setError] = useState('');
const [loading, setloading] = useState(true);

const fetchData = async (params) => {
try {
const result = await axios.request(params);
setResponse(result.data);
} catch( error ) {
setError(error);
} finally {
setLoading(false);
}
};

useEffect(() => {
fetchData(axiosParams);
}, []); // execute once only

return { response, error, loading };
};

useAxiosHook 將取的遠端資料的邏輯包裝在 useAxios.jsx 元件中,並將取得資料後的狀態 return 出來 response, error, loading;

  • 使用 useAxios hook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { useAxios } from 'axioshook';

const App = () => {
const { response, loading, error } = useAxios({
method: 'POST',
url: '/posts',
headers: { // no need to stringify
accept: '*/*'
},
data: { // no need to stringify
userId: 1,
id: 19392,
title: 'title',
body: 'Sample text',
},
});

return (
<div className='App'>
<h1>Posts</h1>

{loading ? (
<p>loading...</p>
) : (
<div>
{error && (
<div>
<p>{error.message}</p>
</div>
)}
<div> {
// no need to use another state to store data, response is sufficient
response && <p>{response.id}</p>
}
</div>
</div>
)}
</div>
);
};

useToggle

專案中也常用到要切換 true 和 false 的情形,可以把切換的邏輯包成 hook

1
2
3
4
5
6
7
8
9
10
11
12
// useToggle.jsx
import { useState } from "react";

export default function useToggle(defaultValue) {
const [value, setValue] = useState(defaultValue);
function toggleValue(value) {
setValue(currentValue => {
return typeof value === 'boolean' ? value : !currentValue;
})
}
return [value, toggleValue];
}

在元件中使用,當 toggleValue 沒有傳入參數時,就切換 value,有傳入 true 或 false,就套用 true或 false。

1
2
3
4
5
6
7
8
9
10
11
12
13
import useToggle from "./useToggle";

export default function ToggleComponent() {
const [value, toggleValue] = useToggle(false);
return (
<div>
<div>{value.toString()}</div>
<button onClick={toggleValue}>Toggle</button>
<button onClick={() => toggleValue(true)}>Make true</button>
<button onClick={() => toggleValue(false)}>Make false</button>
</div>
)
}

useFormInput

react 中表單操作的邏輯也可以包成 hook

1
2
3
4
5
6
7
8
9
10
11
12
export function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);

function handleChange(e) {
setValue(e.target.value);
}
const inputProps = {
value: value,
onChange: handleChange
};
return inputProps;
}

實際使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
return (
<>
<label>
First name:
<input {...firstNameProps} />
</label>
<label>
Last name:
<input {...lastNameProps} />
</label>
<p><b>Good morning, {firstNameProps.value} {lastNameProps.value}.</b></p>
</>
);
}

useTimeout

useTimeoutHook

這段代碼是一個自定義的 React Hook,稱為 useTimeout。它用來處理延遲執行的功能,並提供設置(set)、清除(clear)和重置(reset)計時器的能力。以下是代碼的邏輯分解:

  1. 參數與變數初始化:
  • useTimeout(callback, delay) 接收兩個參數:
    • callback:當計時器到達 delay 時要執行的函數。
    • delay:設定延遲時間(毫秒)。
  • callbackRef 和 timeoutRef 是兩個 useRef 引用,用於保存 callback 函數和計時器的 ID,確保它們在 Hook 中保持一致。
  1. 設定計時器 (set):
  • set 是一個透過 useCallback 儲存的函數,用來設置計時器。
  • 當 set 被調用時,它會根據 delay 使用 setTimeout 來執行 callbackRef.current()。
  • [delay] 為依賴項,當 delay 改變時重新生成 set。
  1. 清除計時器 (clear):
  • clear 也是用 useCallback 儲存的函數,用來清除計時器。
  • 它檢查 timeoutRef.current 是否存在,如果存在則使用 clearTimeout 來清除計時器。
  1. 自動執行 set 和清除計時器:
  • 在第二個 useEffect 中,執行 set 來啟動計時器,並返回 clear 作為清理函數,確保當 delay、set 或 clear 改變時可以自動清除計時器並重新設置。
  • [delay, set, clear] 為依賴項,當這些值變化時會重新執行 useEffect。
  1. 重置計時器 (reset):
  • reset 是一個 useCallback 儲存的函數,先清除計時器,再重新設置計時器。
  • [clear, set] 為依賴項,當 clear 或 set 改變時,重置函數也會重新生成。
  1. 回傳 reset 和 clear:

最後,useTimeout 回傳 reset 和 clear 函數,以便在使用這個 Hook 的組件中可以調用這兩個函數來手動控制計時器。