React自定义hook之(useClickOutside——判断是否点击DOM之外区域)

最近在开发业务需求的时候,有一个场景是点击弹窗之外的区域后,执行某些操作。比如我们常用的github左上角的搜索框,当点击了搜索框之外的区域以后,搜索框就会自动取消搜索并收缩起来。
【React自定义hook之(useClickOutside——判断是否点击DOM之外区域)】React自定义hook之(useClickOutside——判断是否点击DOM之外区域)
文章图片

经过调研发现使用useRef+浏览器事件绑定可以实现这一需求,并且可以将这一功能抽象为自定义hook。
本文将首先介绍如何用传统方式实现这一需求,然后介绍如何抽象成自定义hook,最后结合typescript类型,完善这一自定义hook。
实现检测点击对象外区域
import React, { useEffect, useRef } from "react"; const Demo: React.FC = () => { // 使用useRef绑定DOM对象 const domRef = useRef(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 (); }; 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, 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(null); useClickOutside(domRef, () => { console.log("点击了外部区域"); }); return (); }; export default Demo;

可以看到,组件的代码得到大大的简化,而且抽离出来的自定义hook可以在其他组件中复用。但到这一步有优化的空间吗?当然有的,那就是在类型定义方面,可以使用泛型进行优化。
在刚才编写的useClickOutside自定义hook中,对ref对象的定义是:RefObject,这种定义其实是不合理的,因为HTMLElement不够具体,有可能传入的是HTMLDivElement或者是HTMLAnchorElement,也有可能是HTMLSpanElement,这里我们可以使用泛型对其加以限制。
使用泛型优化类型定义
import { RefObject, useEffect, useRef } from 'react'export function useOnClickOutside( node: RefObject, handler: undefined | (() => void) ) { const handlerRef = useRef 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!

    推荐阅读