React: Custom Hook
在React官方文件開宗明義地說了:
Building your own Hooks lets you extract component logic into reusable functions.
即custom hook就是將程式碼包裝成一個自訂的hook拿來重複利用,就如同使用vanilla JavaScript會把重複程式碼封裝成一個函式重複使用。
至於custom hook是要包裝什麼樣子的程式碼?
先簡單地說,custom hook就是包裝有重複使用到hook的程式碼片段。
Rules of Hook
在寫custom hook以前,可以順便複習一個重要的React hook使用規則:
React內建hooks只能在函式第一層或是costom hook使用。
這段話同時呼應前面為何會說:「custom hook就是包裝有重複使用到hook的程式碼片段。」
Custom Hook
如果程式碼當中有好幾處程式碼同時用相同或類似的邏輯使用hook,這種時候就可以考慮把這些程式碼獨立抽出包裝成custom hook。
舉例來說,我在個人網站每一個段落都用到類似的動畫 ─ 「當使用者垂直捲動捲軸移到該段落就顯示該段落的浮現動畫」,這個效果我是用Web API內建的IntersectionObserver和CSS動畫來實現(可參考我之前的IntersectionObserver使用筆記)。
先來看一段簡化過的程式碼(原始程式碼在這邊):
import { useEffect, useState, useRef } from 'react';
export default function About() {
const [isInView, setIsInView] = useState(false);
const observedRef = useRef<HTMLElement>(null);
useEffect(() => {
if (observedRef.current) {
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
setIsInView(true);
observer.unobserve(entry.target);
} else {
setIsInView(false);
}
},
{
threshold: 0.1,
rootMargin: '-20px',
},
);
observer.observe(observedRef.current);
}
}, [observedRef]);
return <section id="about" ref={observedRef} className={isInView ? '' : 'hidden'}>...</sction>;
個人網站通常會分成好幾個段落,常見的有簡介(about)、作品集(projects)、聯絡資訊(contact)等,不難想像若每個段落都要做動畫,都會用到:
const [isInView, setIsInView] = useState(false);
const observedRef = useRef<HTMLElement>(null);
useEffect(...)
這幾段程式碼。
差別可能只在於使用者捲到不同畫面不同位置, <section ... ref={observedRef} >
要參照(refer)並顯示於畫面的段落標籤會不一樣,這個時候就很適合自訂一個hook來包裝這段程式碼。
1. Define a Custom Hook
使用custom hook就跟平常寫function component差不多,但有以下這些差別:
- 這個function component要以「use」開頭來命名,React看到use開頭component才會判斷這是一個custom hook;
return
不像一般的function component要return JSX標籤,可以回傳任何想回傳的值。
接著來把前面那段程式碼封裝成一個custom hook:
import React, { useState, useEffect } from 'react';
export default function useInView(observedRef) {
const [isInView, setIsInView] = useState(false);
useEffect(() => {
if (observedRef.current) {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
setIsInView(true);
observer.unobserve(entries[0].target);
} else {
setIsInView(false);
}
});
observer.observe(observedRef.current);
}
}, [observedRef]);
return isInView;
}
(以上程式碼同樣有簡化過,原始程式碼在這👈)
可以看到這個custom hook ─ useInView
是以 use 開頭,可以接收一個ref物件 observedRef
,並且在最後 return isInView;
這段回傳 isInView
這個state。
2. Usage of Custom Hook
而使用custom hook也和平常使用React內建的hook一樣:
import useInView from 'use-in-view';
export default function About() {
const observedRef = useRef<HTMLElement>(null);
const isInView = useInView(observedRef);
return <section id="about" ref={observedRef} className={isInView ? '' : 'hidden'}>...</sction>;
}
因為每次顯示動畫要參照的段落標籤不同,每個段落還是要有自己的ref object observedRef
。
不過往下一行 const isInView = useInView(observedRef);
,可以看到observedRef被傳入到 useInView
,並且用一個 isInView
變數來接收回傳的state。
isInView
在 useInView 程式碼裡的初始值是false,不過當使用者拉動捲軸讓畫面顯示到about的段落時,isInView
的值會變成 true,並且回傳至使用這個hook的component當中,也就是 About 這個component。
看到這邊可能會問「這樣使用useInView hook的component會不會同時接收到回傳的true值」,答案是不會,custom hook就和一般內建hook一樣,每個component在使用的custom hook都是獨立的,同時會有自己的state,所以每個custom hook的state會保有自己的值,不會有一個hook的狀態值改變,其他hook的狀態值同時改變的狀況。
【補充?】
可以將宣告custom hook想成是用純JavaScript在宣告函式內的一般區域變數(local variable)一樣,都是一個新的hook變數,每個函式內區域變數都有自己的範疇(scope),因此每個宣告的新hook變數都會被侷限在所處的函式(function component)內,擁有自己的生命週期和state,並不會互有衝突。
References Building Your Own Hooks @React Docs React - The Complete Guide (incl Hooks, React Router, Redux)