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)