基于useRef实现clickoutside

3,258 阅读4分钟

hooks是一种能够在函数式组件中调用的函数,他可以让你在不编写class的情况下使用state以及其他的react特性。

常用的hook有下面这些:

  • useState
  • useEffect
  • useRef
  • useContext
  • useReducer

这篇文章主要讲解如果useRef实现对元素外边点击的检测

useRef

useRef是一个非常强大的基础hook,下面是三个主要的应用场景:

  • 获取DOM元素节点
  • 获取子组件的实例
  • 渲染周期之间共享数据的数据(state不能存储跨渲染周期的数据,因为state的保存会触发组件的重渲染) 在这里我们通过useRef来获取真实的DOM节点

useRef函数接收一个变量用于ref的初始值,然后返回一个ref对象

const elementRef = useRef(null)

然后需要在对应的JSX节点中添加ref属性,这个ref属性值就是调用useRef返回的ref对象。此时需要获取真实DOM,只需要获取到refcurrent属性

const DOM = elementRef.current;

具体如下:

function Index() {
  const elementRef = useRef(null)

  return (
    <>
      <div ref={elementRef}>
        Hello World
      </div>
    </>
  )
}

具体实现

下面来检测一下是否在元素外边发生了点击事件

下面列出了两种场景,我们需要检测是否在元素外边进行了点击:

  • 当创建了一个弹窗,当点击弹窗外的内容,我们需要关闭弹窗
  • 当创建了一个dropdown,当点击dropdown外的内容时候,需要关闭它

下面给一个简单的例子:

function Index() {
    const [isOpen, setIsOpen] = useState(true)
    return (
        <>
            <div>
                <h2>App with a Modal</h2>
                <button onClick={() => setIsOpen(true)}>Open Modal</button>
                <div id="modal">
                  <Modal isOpen={isOpen}>
                    Modal组件内容
                  </Modal>
                </div>
        </>
    )
}

在上面组件中点击按钮展示Modal。我们的目标就是点击modal外边的时候,关闭modal

下面是实现这个目标的具体流程:

  1. 使用ref引用Modal组件

  2. 检测点击

  3. 验证是否在modal组件外发生点击

  4. 如果在Modal组件外发生点击,执行setIsOpen(false)

第一步: 引用Modal

首先使用useRef引用Modal节点

function Index() {
  const [isOpen, setIsOpen] = useState(false);
  const modalRef = useRef();

  return (
    <>
      <div>
        <h2>App with a Modal</h2>
        <button onClick={() => setIsOpen(true)} type="button">
          Open Modal
        </button>
        <div id="modal" ref={modalRef}>
          <Modal isOpen={isOpen}>这是Modal</Modal>
        </div>
      </div>
    </>
  );
}

当当前组件被渲染的时候,我们可以通过modalRef.current就可以获取DOM节点

第二步:添加全局点击事件监听

第二部在全局添加一个事件监听

useEffect(() => {
  function handler(event) {
    console.log(event, 'clicked somewhere')
  }
  window.addEventListener('click', handler)
  return () => window.removeEventListener('click', handler)
}, [])

我们在window上添加了点击监听,监听整个页面的事件。需要注意的是,在组件移除的时候,一定要移除绑定的全局事件,不然可能会造成内存泄漏或者未知的错误

第三步: 检测是否在元素外边发生点击

当事件点击时,回调函数的入参是Event对象,这个对象包含了点击事件的一系列信息。如果要获取到当前点击的元素使用event.target就行。下面检测modal元素是否包含event.target:

useEffect(() => {
  function handler(event) {
    if (!modalRef.current?.contains(event.target)) {
      console.log('clicked outside of the modal')
    }

  }
  window.addEventListener('click', handler)
  return () => window.removeEventListener('click', handler)
}, [])

第四步: 点击modal外的时候,关闭modal

在检测到在modal外点击的时候,执行setIsOpen(false)关闭弹窗

useEffect(() => {
  function handler(event) {
    if (!modalRef.current?.contains(event.target)) {
      console.log('clicked outside of the modal')
    }
  }

  window.addEventListener('click', handler)
  return () => window.removeEventListener('click', handler)
})

封装hook

下面把上面的功能封装成新hook:

export function useOnClickOutside(ref, callback) {
  useEffect(() => {
    function handler(event) {
      if (!ref.current?.contains(event.target)) {
        callback();
      }
    }
    window.addEventListener('click', handler);

    return () => window.removeEventListener('click', handler)
  }, [callback, ref]);
}

在上面hook中,需要传入两个参数: ref: 需要有clickOutside的效果的ref对象 callback: 触发clickOutside的时候的回调函数 函数内部在window上绑定了监听事件,点击时判断点击元素是否在ref对应DOM结构中:如果不是在对应DOM结构中,就触发传入的回调函数。

下面是使用useOnClickOutside hook的函数

function Index() {
  const [isOpen, setIsOpen] = useState(false);
  const ref = useRef(null);
  useOnClickOutside(ref, () => setIsOpen(false));
  return (
    <div>
      <h2>App with ad Modal</h2>
      <button type="button" onClick={() => setIsOpen(false)}>
        Open Modal
      </button>
      <div ref={ref} id="modal">
        <Modal isOpen={isOpen}>This is a Modal</Modal>
      </div>
    </div>
  );
}

在上面代码中,实现了同样的功能。需要更改的添加以下的代码

useOnClickOutside(ref, () => setIsOpen(false));

useRef的使用远不止此。通常我们会使用它来保存变量,这里讲讲useRefuseState的应用场景。在hook中,useStateuseRef都可以用来保存变量,不同的是当useRef的值发生更改时,组件并不会发生重渲染。

github地址: github.com/skychenbo/s… 欢迎关注