(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
在一開始介紹React的時候,我們曾經說過以前React有個問題:
當要使用React的特有功能時,大部份的時候都要做一個元件出來。但有的時候我們並不是要創造元件,而只是要使用React的一兩個特性,卻沒辦法用更直覺、簡單的模組化方式。 又或著只是一個很簡單的元件,卻因為要Follow ES6 class的語法而讓架構看起來很複雜。
這是怎麼回事呢? 我們來看下面這個例子。
過去,如果我們要實作一個多個地方都會使用到的「滑鼠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
多寫一個state
、setState
。
這樣的寫法不但不直覺,也容易讓程式碼變的肥大(在過去因為只能使用class component,此狀況會比上面這個case更嚴重)。
Custom hook的出現解決了這個問題。它的語法相當的簡單
use
開頭。請注意這不是約定成俗。
React會去檢查以use開頭的函式中所使用的hook是不是有違反hook的語法。如果沒有以use開頭,React就無法確認非React component的函式裡面是否有hook
來看實際怎麼撰寫Custom hook就能理解了。
現在,請在src底下新增一個資料夾util
,並創建一個useMouseY.js
接著,把剛剛MouseYListener
的程式碼複製過來,並且做以下更動:
useMouseY
因為custom hook不是React component
mousePosY
直接從函式return出去因為
mousePosY
已經從函式return出去了
mousePosY
因為
mousePosY
已經從函式return出去了
更動完的程式碼會長這樣:
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印出來看看:
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。甚至如果在專案架構設計階段就能考量進去,未來的開發過程會順暢許多、程式碼也會更漂亮。