头图

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

最近在开发业务需求的时候,有一个场景是点击弹窗之外的区域后,执行某些操作。比如我们常用的github左上角的搜索框,当点击了搜索框之外的区域以后,搜索框就会自动取消搜索并收缩起来。

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!


天下事有难易乎? 为者,则难者亦易已;不为,则易者亦难矣!

29 声望
4 粉丝
0 条评论
推荐阅读
基于webpack搭建Chrome扩展开发环境
Chrome扩展开发是目前比较火爆的领域,作为市场占有率第一的浏览器,有海量用户支撑。同时,Chrome扩展可以无缝对接Edge浏览器,不可不谓一箭双雕。 本文将介绍如何使用webpack构建一个支持Typescript的开发环境...

不羁的风阅读 1.3k

封面图
你可能需要的多文档页面交互方案
在日常工作中,面对不同的需求场景,你可能会遇到需要进行多文档页面间交互的实现,例如在 A 页面跳转到 B 页面进行某些操作后,A 页面需要针对该操作做出一定的反馈等等,这个看似简单的功能,却也需要根据不同...

熊的猫8阅读 1.3k

封面图
把React新文档投喂给 GPT-4 后...
大家好,我卡颂。最近,React新文档终于上线了。从内容上看,新文档包括:理论知识、学习指引API介绍从形式上看,新文档除了传统的文字内容,还包括:在线Demo示意图小测验可以说是阅读体验拉满。但是,由于文档...

卡颂7阅读 7.6k评论 3

封面图
第九期:前端九条启发分享
下图是一个常见的列表, 点击列表里的详情按钮会跳到详情页, 那么也许我们在详情页修改了数据状态, 此时可能需要把修改后的状态直接传给列表页从而本地直接更新列表, 这样就不用发送新的api请求与后端交互了。

lulu_up8阅读 887

Next.js-集成状态管理器共享access token以及刷新access token解决方案
SSR和SPA最大的区别就是SSR会区分客户端Client和服务端Server,并且SSR之间只能通过cookie才能在Client和Server之间通信,例如:token信息,以往我们在SPA项目中是使用localStorage或者sessionStorage来存储,但...

Awbeci4阅读 9.2k评论 2

Rollup 基本概念及使用
Rollup是一款基于ESModule模块规范实现的JavaScript打包工具,在前端社区中赫赫有名,同时也在Vite的架构体系中发挥着重要作用。不仅是Vite生产环境下的打包工具,其插件机制也被Vite所兼容,可以说是Vite的构建...

xiangzhihong4阅读 910

不数不知道,React已经有22个hook了
大家好,我卡颂。5月30日刚好是React10周年纪念日。我顺手拉了下React最新代码,这一看不要紧,居然已经有22个hook了。其中:react包导出了21个react-dom包导出了1个(useFormStatus)本文会从React这些年发展脉...

卡颂6阅读 833

封面图

天下事有难易乎? 为者,则难者亦易已;不为,则易者亦难矣!

29 声望
4 粉丝
宣传栏