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.
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!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。