点击弹窗外部自动关闭?一个useRef Hook就搞定!

大家好,我是小杨。在日常的前端开发中,我们经常会遇到这样的需求:点击一个按钮,弹出一个小窗口(Modal),然后希望点击窗口以外的任何区域都能自动关闭它。

这个功能看起来简单,但实现起来却有几个关键的坑点。今天,我就来分享一下我是如何用 React 的 useRefuseEffect 优雅地解决这个问题的。这个方法非常经典,希望你读完也能把它纳入自己的工具库。

为什么不用普通的 state?

首先,可能有新手朋友会想:"我直接用 onClick 监听整个文档(document)的点击事件不就行了?"

想法很好,但实现起来会发现一个问题:事件冒泡。当你点击弹窗内部时,这个点击事件也会冒泡到 document 上,导致弹窗刚一打开就立刻被关闭了。这显然不是我们想要的效果。

所以,我们需要一个办法来判断一次点击事件,究竟是不是发生在弹窗的外部

核心思路:useRef 来充当"坐标轴"

我的解决方案是:

  1. 使用 useRef 为弹窗组件创建一个引用(ref)。
  2. useEffect 中,给 document 添加点击事件监听。
  3. 当任何点击事件发生时,检查被点击的元素(event.target)是否包含在我们弹窗的 ref 中。
  4. 如果不包含,说明点击发生在弹窗外部,触发关闭函数。

useRef 在这里扮演了至关重要的角色,因为它可以让我们直接访问到真实的 DOM 节点,而且它的值在组件生命周期内保持不变,不会引起额外的渲染,非常适合用来做这种 DOM 相关的判断。

Show Me The Code!

下面是我在一个实际项目中写的自定义 Hook,我把它命名为 useClickOutside。它的职责就是封装这个逻辑,让任何需要"点击外部关闭"的组件都能轻松复用。

jsx 复制代码
import { useEffect, useRef } from 'react';

const useClickOutside = (handler) => {
  const ref = useRef(null);

  useEffect(() => {
    const handleClickOutside = (event) => {
      // 检查 ref 当前是否已经关联到DOM元素
      // 再检查点击事件的目标是否在 ref 所指向的DOM元素内部
      if (ref.current && !ref.current.contains(event.target)) {
        // 如果在外部,则调用传入的处理函数
        handler();
      }
    };

    // 添加事件监听
    document.addEventListener('mousedown', handleClickOutside);

    // 清除副作用:组件卸载时移除事件监听
    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, [handler]); // 只有当 handler 改变时,才重新创建 effect

  return ref;
};

export default useClickOutside;

代码解读:

  1. const ref = useRef(null);: 创建一个 ref 对象,初始化为 null
  2. useEffect: 在组件挂载后执行。
  3. handleClickOutside: 定义事件处理函数。ref.current.contains(event.target) 是核心逻辑,用于判断点击目标是否在 ref 指向的节点内部。
  4. document.addEventListener(...): 监听整个 document 的 mousedown 事件(比 click 更早触发)。
  5. return () => { ... }: 这是 effect 的清理函数,在组件卸载时执行,移除事件监听,防止内存泄漏。
  6. return ref: 最后,将这个 ref 返回出去,让使用这个 Hook 的组件能够绑定到具体的 DOM 节点上。

在弹窗组件中如何使用?

有了这个自定义 Hook,我们的弹窗组件就变得非常简洁和易用了。

jsx 复制代码
import React, { useState } from 'react';
import useClickOutside from './useClickOutside'; // 假设Hook放在同目录

const Modal = ({ isOpen, onClose, children }) => {
  // 使用自定义Hook,传入关闭弹窗的函数
  const modalRef = useClickOutside(onClose);

  // 如果 isOpen 为 false,则不渲染任何内容
  if (!isOpen) return null;

  return (
    <div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center">
      {/* 将返回的 ref 绑定到最外层的 div 上 */}
      <div ref={modalRef} className="bg-white p-8 rounded-lg shadow-xl">
        {children}
        <button 
          onClick={onClose} 
          className="mt-4 px-4 py-2 bg-blue-500 text-white rounded"
        >
          关闭
        </button>
      </div>
    </div>
  );
};

export default Modal;

使用场景:

jsx 复制代码
function App() {
  const [isModalOpen, setModalOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setModalOpen(true)}>打开弹窗</button>
      <Modal 
        isOpen={isModalOpen} 
        onClose={() => setModalOpen(false)}
      >
        <h2>我是弹窗内容!</h2>
        <p>点击我内部不会关闭,点击外部就会自动关闭哦~</p>
      </Modal>
    </div>
  );
}

总结与思考

通过这个例子,我们可以看到 useRef 的强大之处:

  • 操作 DOM: 这是它的老本行,直接获取组件实例或 DOM 节点。
  • 存储可变值 : 它可以存储任何可变值,且值的改变不会触发组件重新渲染。这使得它非常适合用来保存那些你需要在渲染过程中保留,但又不想引起重新渲染的数据(例如 setTimeout 的 ID、上一次的 props 值等)。

"点击外部关闭"这个功能只是 useRef 的冰山一角。理解了它的这种能力,你就能在诸如集成第三方 DOM 库、管理焦点、触发动画等场景中游刃有余。

⭐ 写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

相关推荐
IT_陈寒5 分钟前
Python开发者必知的5个高效技巧,让你的代码速度提升50%!
前端·人工智能·后端
zm43521 分钟前
浅记Monaco-editor 初体验
前端
超凌23 分钟前
vue element-ui 对表格的单元格边框加粗
前端
渊不语25 分钟前
PasteTextArea 智能文本域粘贴组件 - 完整实现指南
react.js
前端搬运侠25 分钟前
🚀 TypeScript 中的 10 个隐藏技巧,让你的代码更优雅!
前端·typescript
CodeTransfer26 分钟前
css中animation与js的绑定原来还能这样玩。。。
前端·javascript
liming49527 分钟前
运行node18报错
前端
202629 分钟前
14.7 企业级脚手架-制品仓库发布使用
前端·vue.js
coding随想36 分钟前
揭秘HTML5的隐藏开关:监控资源加载状态readyState属性全解析!
前端