You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义IntersectionObserver Hook的exhaustive-deps警告修复方案

React IntersectionObserver自定义Hook exhaustive-deps警告排查与修复

背景

参考dev.to平台相关教程,实现了可在React中使用IntersectionObserver的自定义Hook,用于监听元素是否进入视口。

原始实现代码

import { useEffect, useRef, useState } from "react"


export const useIntersectionObserver = (options) => {
  const containerRef = useRef(null)
  const [isVisible, setIsVisible] = useState(false)

  const callback = (entries) => {
    const [entry] = entries
    setIsVisible(entry.isIntersecting)
  }


  useEffect(() => {
    let currentEl;

    const observer = new IntersectionObserver(callback, options);

    if (containerRef && containerRef.current) {
      currentEl = containerRef.current;
      observer.observe(currentEl);
    }

    return () => {
      if (containerRef && containerRef.current) {
        currentEl = containerRef.current;
        observer.unobserve(currentEl);
      }
    }
  }, [containerRef, options])

  return [containerRef, isVisible]
}

触发的ESLint警告

运行代码时控制台抛出react-hooks/exhaustive-deps规则警告:

The ref value 'containerRef.current' will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy 'containerRef.current' to a variable inside the effect, and use that variable in the cleanup function
中文释义:effect清理函数执行时,containerRef.current的值大概率已经发生变更。如果该ref指向React渲染的DOM节点,需要在effect作用域内部将containerRef.current赋值给局部变量,清理函数统一使用该局部变量。

警告触发的深层原理

代码虽然声明了currentEl局部变量,但依然触发警告,核心问题出在清理函数的逻辑:

  • useRef返回的ref对象本身引用是稳定的,组件全生命周期内不会变化,但它的.current属性是完全可变的,React不会在.current变化时触发重渲染,也不会触发effect重新执行。
  • effect的执行时机是依赖项变更后、DOM提交到页面完成时;清理函数的执行时机是下一次effect运行前,或是组件卸载时。两次执行之间可能间隔多次重渲染,此时containerRef.current可能已经指向其他DOM节点,甚至变为null。
  • 原始代码中,虽然在绑定observer时把containerRef.current赋值给了currentEl,但在清理函数里又重新读取containerRef.current覆盖了currentEl的值,此时拿到的元素根本不是当初调用observe绑定的目标元素:一方面会导致旧绑定元素没有被正确解绑,造成内存泄漏;另一方面可能对从未绑定过observer的新元素错误调用unobserve。ESLint规则正是检测到了清理函数中对ref可变属性的不安全读取,才抛出警告。
  • 另外containerRef本身是稳定引用,放进依赖数组没有实际意义,不会触发effect重执行。

符合React Hooks规范的修复方案

修复核心逻辑:只在effect执行、绑定observer的时机读取一次containerRef.current,存为effect作用域内的局部常量,整个effect生命周期(包括清理阶段)全程使用这个常量,不在清理函数中重新读取containerRef.current。
修复后代码:

import { useEffect, useRef, useState } from "react"


export const useIntersectionObserver = (options) => {
  const containerRef = useRef(null)
  const [isVisible, setIsVisible] = useState(false)

  const callback = (entries) => {
    const [entry] = entries
    // 用函数式更新避免callback依赖isVisible状态
    setIsVisible(() => entry.isIntersecting)
  }


  useEffect(() => {
    // 仅在effect执行时读取一次ref指向的元素,存为作用域常量
    const currentEl = containerRef.current
    const observer = new IntersectionObserver(callback, options)

    if (currentEl) {
      observer.observe(currentEl)
    }

    return () => {
      // 清理时直接使用作用域内存储的元素引用,不再读取ref.current
      if (currentEl) {
        observer.unobserve(currentEl)
      }
    }
  }, [options, callback])

  return [containerRef, isVisible]
}

额外优化点

如果需要进一步减少不必要的observer重建,可以用useCallback包裹callback函数,确保其引用稳定;上述代码中已经使用setIsVisible的函数式更新写法,让callback不依赖任何外部状态,本身引用就是稳定的,effect只会在options变更时重新创建observer实例,性能更优。

内容的提问来源于stack exchange,提问作者Ben_Sven_Ten

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 03:09:15