头图

React custom hook: useClickOutside - determine whether to click on the area outside the DOM

When developing business requirements recently, there is a scenario where certain operations are performed after clicking on an area outside the pop-up window. For example, the search box in the upper left corner of github that we commonly use, when the area outside the search box is clicked, the search box will automatically cancel the search and shrink.

github搜索框

After research, it is found that this requirement can be achieved using useRef + browser event binding , and this function can be abstracted into a custom hook.

This article will first introduce how to implement this requirement in a traditional way, then introduce how to abstract it into a custom hook, and finally combine the typescript type to improve this custom hook.

Realize the detection of the area outside the clicked object

 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;

The code is not difficult to understand. First, we wrote a square with a length and width of 300 pixels in the functional component, then created an object named domRef to bind it to the dom node, and finally declared handleClickOutSide in the useEffect hook. The method of judging whether the user has clicked on the specified DOM area, and using the document.addEventListener method to add event listeners, clean up event listeners when the component is unloaded.
In the process of implementation, the core is to use the contains method on the Ref object. After research, it is found that the Node.contains method is the native method of the browser, and its main function is to judge whether the incoming DOM node belongs to the node. descendant nodes.

After using the basic method to implement, then encapsulate the custom hook.

Encapsulate useClickOutside hook

You don't have to be afraid when you understand custom hooks. It's nothing more than calling ordinary functions of other hooks. Let's look at the code implementation:

 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;

It can be seen that the code extracts the logic of judging whether the area outside the DOM is clicked, so that when using it, you only need to pass the DOM node to useClickOutside, and the second parameter of the custom hook receives a callback function. You can do various things inside the callback function. Let's take a look at the code after using the custom 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;

It can be seen that the code of the component is greatly simplified, and the extracted custom hook can be reused in other components. But is there room for optimization at this point? Of course there are, that is, in terms of type definitions, generics can be used for optimization.
In the useClickOutside custom hook just written, the definition of the ref object is: RefObject<HTMLElement>, this definition is actually unreasonable, because HTMLElement is not specific enough, it may be HTMLDivElement or HTMLAnchorElement, or it may be HTMLSpanElement, here we can use generics to limit it.

Optimizing type definitions with generics

 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])
}

In the optimized hook, the generic type T is used to refer to the incoming type inherited from HTMLElement, so that when declaring the ref object, T can be used to refer to the incoming DOM type. Using the generic type can strengthen the constraints on the incoming parameters. , you can try more in the project development.
In the handleClickOutside method, the double question mark ?? is used to judge. The double question mark means that if the previous part is undefined, the following content is returned, that is, false.
The last full version of the useClickOutside hook in this article draws on the uniswap open source project
project address

Thank you for reading, if you think it's good, please like o( ̄▽ ̄)d!


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

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

不羁的风阅读 1.3k

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

熊的猫8阅读 1.2k

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

卡颂7阅读 7.5k评论 3

封面图
PDF 预览和下载你是怎么实现的?
在开发过程中要求对 PDF 类型的发票提供 预览 和 下载 功能,PDF 类型文件的来源又包括 H5 移动端 和 PC 端,而针对这两个不同端的处理会有些许不同,下文会有所提及。

熊的猫7阅读 3.7k评论 1

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

lulu_up8阅读 839

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

Awbeci4阅读 9k评论 2

3个容易混淆的前端框架概念
大家好,我卡颂。有3个容易混淆的前端框架概念:响应式更新单向数据流双向数据绑定在继续阅读本文前,读者可以思考下是否明确知道三者的含义。这三者之所以容易混淆,是因为他们虽然同属前端框架范畴内的概念,但...

卡颂6阅读 910

封面图

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

29 声望
4 粉丝
宣传栏