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

useInterval回调中loading变量值异常的原因及修复方案

React组件useInterval回调中loading始终为true的原因与修复

在InfoPage组件中,useInterval回调函数内的loading变量始终显示为true,但组件外部传入的loading prop最终会变为false,控制台输出也验证了这一差异。

组件代码

const InfoPage = ({ loading, fetchDataFn }) => {
  const [lastIntervalTs, setLastIntervalTs] = useState(new Date().valueOf())
  const [lastFetchTs, setLastFetchTs] = useState(new Date().valueOf())

  const refetchData = () => {
    const ts = new Date().valueOf()
    setLastFetchTs(ts)
    setLastIntervalTs(ts)

    fetchDataFn()
  }

  console.log('Outside callback', loading)

  useInterval(() => {
    console.log('Inside callback', loading)
    if (!loading) {
      setLastIntervalTs(new Date().valueOf())
    }
  }, 5000)

  const currentTs = new Date().valueOf()
  if (currentTs - lastFetchTs > 15000) {
    refetchData()
  }
}

useInterval自定义Hook代码

const useInterval = (callback, delay = 0) => {
  const savedCallbackRef = useRef(callback)
  const intervalIdRef = useRef()

  useEffect((): void => {
    savedCallbackRef.current = callback
  }, [callback])

  useEffect(() => {
    intervalIdRef.current && clearInterval(intervalIdRef.current)
    if (delay != null) {
      intervalIdRef.current = setInterval(savedCallbackRef.current, delay)
    }
    return () => intervalIdRef.current && clearInterval(intervalIdRef.current)
  }, [delay])
}

问题原因

核心问题出在useInterval的实现逻辑上:

  • 创建定时器时,setInterval直接传入了savedCallbackRef.current(即组件第一次渲染时生成的回调函数),这个函数通过闭包捕获了当时的loading值(true)。
  • 后续组件重新渲染时,虽然useEffect会将savedCallbackRef.current更新为新的回调函数,但setInterval保存的是最初传入的旧函数引用,不会自动切换到新的回调。因此定时器每次触发时,始终执行旧回调,导致loading值一直停留在最初的true。

修复方案

有两种可行的修复方式,推荐第一种(性能更优,无需频繁销毁重建定时器):

方式一:修改useInterval,动态获取最新回调

将setInterval的回调改为匿名函数,内部调用savedCallbackRef.current,这样每次定时器触发时都会执行最新的回调函数:

const useInterval = (callback, delay = 0) => {
  const savedCallbackRef = useRef(callback)
  const intervalIdRef = useRef()

  useEffect((): void => {
    savedCallbackRef.current = callback
  }, [callback])

  useEffect(() => {
    intervalIdRef.current && clearInterval(intervalIdRef.current)
    if (delay != null) {
      // 用匿名函数包裹,动态获取最新的回调
      intervalIdRef.current = setInterval(() => {
        savedCallbackRef.current()
      }, delay)
    }
    return () => intervalIdRef.current && clearInterval(intervalIdRef.current)
  }, [delay])
}

方式二:让useInterval监听callback变化,重建定时器

修改useInterval的第二个useEffect依赖项,加入callback,这样当回调函数变化时,自动销毁旧定时器并创建新定时器,传入最新的回调:

const useInterval = (callback, delay = 0) => {
  const savedCallbackRef = useRef(callback)
  const intervalIdRef = useRef()

  useEffect((): void => {
    savedCallbackRef.current = callback
  }, [callback])

  useEffect(() => {
    intervalIdRef.current && clearInterval(intervalIdRef.current)
    if (delay != null) {
      intervalIdRef.current = setInterval(savedCallbackRef.current, delay)
    }
    return () => intervalIdRef.current && clearInterval(intervalIdRef.current)
  }, [delay, callback]) // 加入callback作为依赖
}

两种方式都能解决闭包陷阱问题,让回调获取到最新的loading值。方式一避免了频繁销毁和创建定时器,性能更优;方式二更直观,但每次回调变化都会重建定时器。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 22:05:03