最近在開發業務需求的時候,有一個場景是點選彈窗之外的區域後,執行某些操作。比如我們常用的github左上角的搜尋框,當點選了搜尋框之外的區域以後,搜尋框就會自動取消搜尋並收縮起來。
經過調研發現使用useRef+瀏覽器事件繫結可以實現這一需求,並且可以將這一功能抽象為自定義hook。
本文將首先介紹如何用傳統方式實現這一需求,然後介紹如何抽象成自定義hook,最後結合typescript型別,完善這一自定義hook。
實現檢測點選物件外區域
import React, { useEffect, useRef } from "react";
const Demo: React.FC = () => {
// 使用useRef繫結DOM物件
const domRef = useRef<HTMLDivElement>(null);
// 元件初始化繫結點選事件
useEffect(() => {
const handleClickOutSide = (e: MouseEvent) => {
// 判斷使用者點選的物件是否在DOM節點內部
if (domRef.current?.contains(e.target as Node)) {
console.log("點選了DOM裡面區域");
return;
}
console.log("點選DOM外面區域");
};
document.addEventListener("mousedown", handleClickOutSide);
return () => {
document.removeEventListener("mousedown", handleClickOutSide);
};
}, []);
return (
<div
ref={domRef}
style={{
height: 300,
width: 300,
background: "#bfa",
}}
></div>
);
};
export default Demo;
程式碼不難理解,首先我們在函式式元件裡面寫了一個長度和寬度都是300畫素的正方形,然後建立了一個名為domRef的物件將其繫結到dom節點上,最後在useEffect鉤子裡面宣告handleClickOutSide的方法判斷使用者是否點選了指定的DOM區域,並使用document.addEventListener方法新增事件監聽,元件解除安裝時清理事件監聽。
在實現的過程中,最核心的是利用了Ref物件上的contains方法,經過研究發現,Node.contains方法是瀏覽器的原生方法,其主要的作用是判斷傳入的DOM節點是否為該節點的後代節點。
使用基本方式實現後,接著封裝自定義hook。
封裝useClickOutside hook
大家在理解自定義hook時不用心生畏懼,無非就是呼叫了其他hook的普通函式,下面來看程式碼實現:
import { RefObject, useEffect } from "react";
const useClickOutside = (ref: RefObject<HTMLElement>, handler: Function) => {
useEffect(() => {
const listener = (event: MouseEvent) => {
if (!ref.current || ref.current.contains(event.target as HTMLElement)) {
return;
}
handler(event);
};
document.addEventListener("click", listener);
return () => {
document.removeEventListener("click", listener);
};
}, [ref, handler]);
};
export default useClickOutside;
可以看到,程式碼將判斷是否點選了DOM之外區域的邏輯都抽離出來,這樣在使用時,只需要把DOM節點傳遞給useClickOutside即可,自定義hook的第二個引數接收一個回撥函式,可以在回撥函式裡面做各種事情。來看一下使用自定義hook後的程式碼:
import React, { useRef } from "react";
import useClickOutside from "../hooks/useOnClickOutside";
const Demo: React.FC = () => {
// 使用useRef繫結DOM物件
const domRef = useRef<HTMLDivElement>(null);
useClickOutside(domRef, () => {
console.log("點選了外部區域");
});
return (
<div
ref={domRef}
style={{
height: 300,
width: 300,
background: "#bfa",
}}
></div>
);
};
export default Demo;
可以看到,元件的程式碼得到大大的簡化,而且抽離出來的自定義hook可以在其他元件中複用。但到這一步有優化的空間嗎?當然有的,那就是在型別定義方面,可以使用泛型進行優化。
在剛才編寫的useClickOutside自定義hook中,對ref物件的定義是:RefObject<HTMLElement>,這種定義其實是不合理的,因為HTMLElement不夠具體,有可能傳入的是HTMLDivElement或者是HTMLAnchorElement,也有可能是HTMLSpanElement,這裡我們可以使用泛型對其加以限制。
使用泛型優化型別定義
import { RefObject, useEffect, useRef } from 'react'
export function useOnClickOutside<T extends HTMLAnchorElement>(
node: RefObject<T | undefined>,
handler: undefined | (() => void)
) {
const handlerRef = useRef<undefined | (() => void)>(handler)
useEffect(() => {
handlerRef.current = handler
}, [handler])
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (node.current?.contains(e.target as Node) ?? false) {
return
}
if (handlerRef.current) handlerRef.current()
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [node])
}
優化後的hook裡,使用泛型T指代傳入的型別繼承自HTMLElement,這樣在宣告ref物件的時候就可以用T指代傳入的DOM型別,使用泛型可以加強對傳入引數的約束,大家在專案開發中可以多加嘗試。
在handleClickOutside方法中,使用了雙問號??判斷,雙問號的意思是如果前面的部分為undefined則返回後面的內容,也就是false。
本文最後完整版的useClickOutside hook借鑑了uniswap開源專案
專案地址
謝謝閱讀,如果覺得不錯,歡迎點贊o( ̄▽ ̄)d!