iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 25
1
Modern Web

從比入門再往前一點開始,一直到深入React.js系列 第 25

【Day.25】React進階 - Custom hook | 把React component API模組化吧!

  • 分享至 

  • twitterImage
  •  

(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問


在一開始介紹React的時候,我們曾經說過以前React有個問題:

當要使用React的特有功能時,大部份的時候都要做一個元件出來。但有的時候我們並不是要創造元件,而只是要使用React的一兩個特性,卻沒辦法用更直覺、簡單的模組化方式。 又或著只是一個很簡單的元件,卻因為要Follow ES6 class的語法而讓架構看起來很複雜。

這是怎麼回事呢? 我們來看下面這個例子。

滑鼠位置監聽器的實現(沒有Custom hook)

過去,如果我們要實作一個多個地方都會使用到的「滑鼠Y位置監聽器」模組,由於要使用:

  • state: 來讓元件能因為滑鼠Y位置的改變,而做對應的更新。
  • 生命週期: 在元件載入時監聽mousemove,元件移除時移除監聽事件,避免重複監聽。

所以我們必須一定要使用React component,不然這些React component API會不能使用。實作起來的程式碼長這樣:

import React, {useState, useEffect, useCallback} from 'react';

function MouseYListener(props){
    const [mousePosY, setMousePosY] = useState(0);
    
    // 由於useState給的setMousePosY的ref不會變,所以此函式不會改變
    const mouseListener = useCallback((event)=>{
        setMousePosY(event.pageY);
    },[setMousePosY]);

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

    // 呼叫props函式,讓使用它的父元件可以得到mousePosY
    useEffect(()=>{
        // 確保有綁handleMouseMove在props上
        if(props.handleMouseMove) 
            props.handleMouseMove(mousePosY);
    },[mousePosY])

    return <></>;
}

export default MouseYListener;

然後在父元件,我們就要這樣用MouseYListener:

function ParentComponent(){
    const [mouseYPos,setMouseYPos] = useState(0);
    return 
        <>
            <OtherComponent mouseYPos={mouseYPos}/>
            <MouseYListener handleMouseMove={setMouseYPos}/>
        </>
}

但是我們的MouseYListener明明沒有渲染任何DOM元素,卻要以標籤形式寫在程式碼中。而且我們還要在ParentComponent多寫一個statesetState

這樣的寫法不但不直覺,也容易讓程式碼變的肥大(在過去因為只能使用class component,此狀況會比上面這個case更嚴重)。

Custom hook

Custom hook的出現解決了這個問題。它的語法相當的簡單

  • 必須是函式
  • 可以在裡面使用React hook
  • 使用的規則和React hook相同(如: 只能在function component呼叫)
  • 名稱必須是以use開頭。請注意這不是約定成俗

    React會去檢查以use開頭的函式中所使用的hook是不是有違反hook的語法。如果沒有以use開頭,React就無法確認非React component的函式裡面是否有hook

你可以用Custom hook把React component特性模組化。當你在不同的React function component中引用時,每一個function component中的Custom hook會獨立運作、不受彼此影響,而且你不用重複撰寫Custom hook的定義、也不用回傳JSX。

來看實際怎麼撰寫Custom hook就能理解了。

滑鼠位置監聽器的實現(有Custom hook)

現在,請在src底下新增一個資料夾util,並創建一個useMouseY.js

接著,把剛剛MouseYListener的程式碼複製過來,並且做以下更動:

  • 把函式名稱改成useMouseY
  • 不需要引入React,只需要引入React hook

    因為custom hook不是React component

  • mousePosY直接從函式return出去
  • 不需要用props接收handleMouseMove

    因為mousePosY已經從函式return出去了

  • 不需要用一個useEffect來讓父元件得到mousePosY

    因為mousePosY已經從函式return出去了

更動完的程式碼會長這樣:

  • src/util/useMouseY.js
import {useState, useEffect, useCallback} from 'react';

function useMouseY(){
    const [mousePosY, setMousePosY] = useState(0);
    
    const mouseListener = useCallback((event)=>{
        setMousePosY(event.pageY);
    },[setMousePosY]);

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

    return mousePosY;
}

export default useMouseY;

接著,我們就能在其他地方引用,例如在src/page/MenuPage中:

import useMouseY from '../util/useMouseY';

直接接收回傳值即可

const MenuPage = () =>{
    const mousePosY = useMouseY();
    /* 以下略 */

你可以用useEffect印出來看看:

  • src/page/MenuPage.js
import React, {useState,useMemo,useEffect} from 'react';
import useMouseY from '../util/useMouseY';

import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';

let menuItemWording=[
    "Like的發問",
    "Like的回答",
    "Like的文章",
    "Like的留言"
];

const MenuPage = () =>{
    const mousePosY = useMouseY();
    
    const [isOpen, setIsOpen] = useState(true);
    const [menuItemData, setMenuItemData] = useState(menuItemWording);
    let menuItemArr = useMemo(()=>menuItemData.map((wording) => <MenuItem text={wording} key={wording}/>),[menuItemData]);

    useEffect(()=>{
        console.log(mousePosY);
    },[mousePosY])

    return (
        <OpenContext.Provider value={{ 
            openContext: isOpen, 
            setOpenContext: setIsOpen
        }} >
            <Menu title={"Andy Chang的like"}>
                {menuItemArr}
            </Menu>
            <button onClick={()=>{
                let menuDataCopy = ["測試資料"].concat(menuItemData);
                setMenuItemData(menuDataCopy); 
            }}>更改第一個menuItem</button>
        </OpenContext.Provider>
    );
}

export default MenuPage;


小結

Custom hook讓React程式模組化變的更加直覺,這也是React社群這兩年強烈推薦捨棄class component的原因之一。許多第三方插件也利用Custom hook打造出了相容React function component的API。

當你在多個Component都有使用到相同的React邏輯時,就應該要把其拆出來做成custom hook。甚至如果在專案架構設計階段就能考量進去,未來的開發過程會順暢許多、程式碼也會更漂亮。


上一篇
【Day.24】React效能 - 用lazy和Suspense來動態載入元件
下一篇
【Day.26】React進階 - useEffect v.s useLayoutEffect
系列文
從比入門再往前一點開始,一直到深入React.js30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
AndrewYEE
iT邦新手 3 級 ‧ 2023-02-04 14:42:59

很棒! 順便讓我搞懂useCallback! 目前在配合你的書在學,謝謝!

我要留言

立即登入留言