一个关于如何通过为React组件创建一个自定义的React钩子来检测其外部的点击的教程。例如,你可能需要为各种组件(如对话框或下拉菜单)定制这样的React钩子,因为当用户在它们之外点击时,它们应该关闭。所以我们需要一种方法来找出这种外部点击。
你在这里要学习的大部分内容都可以追溯到JavaScript中的事件冒泡和捕捉的概念。所以,如果你需要复习一下冒泡、目标和捕捉阶段,我建议你先阅读下面这篇文章,它涉及到React的这个主题。
继续阅读。React中的事件捕获和起泡
让我们用React中的一个函数组件来开始吧,我们通过使用React的useState Hook和一个事件处理程序来增加一个计数器。
import * as React from 'react';
const style = {
padding: '10px',
border: '1px solid black',
display: 'flex',
justifyContent: 'flex-end',
};
function App() {
const [count, setCount] = React.useState(0);
const handleClick = () => {
setCount((state) => state + 1);
};
return (
<div style={style}>
<button type="button" onClick={handleClick}>
Count: {count}
</button>
</div>
);
}
export default App;
Count:0
一切都按预期进行。接下来,我们想在用户点击按钮之外的地方重置状态(这里是:count )。我们可以写出重置状态的事件处理程序,然而,目前还不清楚在哪里使用它。
function App() {
const [count, setCount] = React.useState(0);
const handleClickOutside = () => {
setCount(0);
};
const handleClick = () => {
setCount((state) => state + 1);
};
return (
<div style={style}>
<button type="button" onClick={handleClick}>
Count: {count}
</button>
</div>
);
}
一个天真的方法是在顶层组件的最外层HTML元素上使用这个新的处理程序(这里:<div> )。然而,一个更好的方法是在文档层面上使用这个事件处理程序作为最佳实践,因为最外层的HTML元素在开发过程中会发生变化。
我们将在一个自定义钩子中直接实现这一点,以避免多余的重构。
const useOutsideClick = (callback) => {
const ref = React.useRef();
React.useEffect(() => {
const handleClick = (event) => {
callback();
};
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
};
}, []);
return ref;
};
自定义钩子启动了一个React ref,最终被返回。在钩子的实现细节中还没有真正用到。此外,自定义钩子使用React的useEffect钩子来分配(和删除)一个文档级别的事件监听器(这里:点击事件)。毕竟,每当document 被点击,处理程序,从而传递的回调函数将运行。
现在,自定义钩子可以在我们的React组件中以如下方式使用:将事件处理程序作为回调函数传递给钩子--只要文档被点击就会执行。此外,使用返回的引用(这里是:ref )并将其分配给按钮的HTML元素。
function App() {
const [count, setCount] = React.useState(0);
const handleClickOutside = () => {
setCount(0);
};
const ref = useOutsideClick(handleClickOutside);
const handleClick = () => {
setCount((state) => state + 1);
};
return (
<div style={style}>
<button ref={ref} type="button" onClick={handleClick}>
Count: {count}
</button>
</div>
);
}
然而,你会注意到,处理程序总是会启动,当按钮本身被点击的时候也是如此。如果你再检查一下自定义钩子,你会发现在那里并没有真正使用引用(读作:ref )。我们想要达到的目的。只有当传递的ref (这里代表按钮)之外的任何东西被点击时,才执行回调函数,而不是当ref 本身(或其内容)被点击时。
const useOutsideClick = (callback) => {
const ref = React.useRef();
React.useEffect(() => {
const handleClick = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
callback();
}
};
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
};
}, [ref]);
return ref;
};
这就是了。分配给按钮的引用是触发按钮的事件处理程序和文档的事件处理程序之间的边界。所有在引用之外的点击都将被认为是外部点击。
不过还缺少一个小小的改进。如果我们需要通过在事件处理程序上使用stopPropagation() 方法来停止某些边缘情况下的事件冒泡,该怎么办。例如,在下面的例子中,我们通过点击容器元素来扩展组件,并在那里停止事件的传播。
const style = {
padding: '10px',
border: '1px solid black',
display: 'flex',
justifyContent: 'space-between',
};
...
function App() {
const [count, setCount] = React.useState(0);
const handleClickOutside = () => {
setCount(0);
};
const ref = useOutsideClick(handleClickOutside);
const handleClick = () => {
setCount((state) => state + 1);
};
const handleHeaderClick = (event) => {
// do something
event.stopPropagation();
};
return (
<div style={style} onClick={handleHeaderClick}>
<div>Header</div>
<button ref={ref} type="button" onClick={handleClick}>
Count: {count}
</button>
</div>
);
}
当我们尝试这个例子时,我们会看到对容器的点击并没有作为 "外部点击 "通过,因为即使它是一个外部点击,由于事件被停止冒泡,它从未到达文档的事件监听器。
通过利用冒泡和捕捉阶段,我们可以调整自定义钩子,使其在捕捉阶段启动。因为捕获阶段发生在冒泡阶段之前,所以即使事件在冒泡阶段被停止传播,对文档的点击也会一直运行。
const useOutsideClick = (callback) => {
const ref = React.useRef();
React.useEffect(() => {
const handleClick = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
callback();
}
};
document.addEventListener('click', handleClick, true);
return () => {
document.removeEventListener('click', handleClick, true);
};
}, [ref]);
return ref;
};
这就是了。你创建了一个自定义的钩子,它可以检测引用的组件/元素之外的点击行为。再次,请阅读事件冒泡和捕捉的文章,以获得对这些阶段发生的事情的更深入解释。
最后但并非最不重要的是,你可能想退回到一个库来处理这个问题。你总是可以自己实现自定义钩子--这是一个锻炼的好方法,也是一个了解引擎盖下实现细节的好方法--然而,如果有一个无懈可击的库来管理所有的边缘情况(见前面的捕获/冒泡边缘情况),你应该利用它。