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

useEffect实现viewport自定义Hook内存泄漏问题排查

React useEffect自定义Hook内存泄漏警告排查

问题根因

控制台抛出的状态更新警告,本质是组件卸载后仍有异步任务执行setState,核心问题出在防抖逻辑的清理缺失:

  • 现有debounce实现所有执行逻辑都通过setTimeout延迟触发,包括挂载时主动调用的初始化逻辑,不会立刻执行目标函数
  • useWindowSize的effect清理逻辑仅移除了resize事件监听,没有清除防抖函数内部排队的定时器
  • 若组件在防抖的300ms等待窗口内卸载(比如路由快速跳转、条件渲染移除组件),定时器到期后依然会执行setWindowSize更新已卸载组件的状态,直接触发警告
  • 额外问题:首次加载时需要等300ms才能拿到窗口尺寸,首屏会出现短暂的viewport值为空的情况

修复步骤

1. 改造debounce工具函数,增加取消能力

给防抖函数增加cancel方法,用于主动清除待执行的定时器,这是通用防抖工具的标准能力:

// helpers/helpers 中debounce修改后代码
export function debounce<U = unknown, V = void>(
  fn: (args: U) => V,
  wait: number
) {
  let timer: ReturnType<typeof setTimeout> | null = null

  const debounced = (args: U): Promise<V> =>
    new Promise((resolve) => {
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
        resolve(fn(args))
        timer = null
      }, wait)
    })

  // 新增取消方法,清理待执行任务
  debounced.cancel = () => {
    if (timer) {
      clearTimeout(timer)
      timer = null
    }
  }

  return debounced
}

2. 修复useWindowSize逻辑

初始化直接读取window尺寸避免首屏等待,卸载时调用cancel方法清除待执行的防抖任务:

import { useState, useEffect } from "react"
import { debounce } from "helpers/helpers"

export function useWindowSize() {
  // 初始化直接取window值,兼容SSR场景判断
  const [windowSize, setWindowSize] = useState({
    width: typeof window !== 'undefined' ? window.innerWidth : undefined,
    height: typeof window !== 'undefined' ? window.innerHeight : undefined,
  })

  useEffect(() => {
    function handleResize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      })
      const vh = (window.top || window).innerHeight * 0.01
      document.documentElement.style.setProperty("--vh", `${vh}px`)
    }
    const debouncedHandleResize = debounce(handleResize, 300)

    window.addEventListener("resize", debouncedHandleResize)
    // 初始化直接执行一次,不走防抖延迟
    handleResize()

    return () => {
      window.removeEventListener("resize", debouncedHandleResize)
      // 卸载时清除所有待执行的防抖回调
      debouncedHandleResize.cancel()
    }
  }, [])

  return windowSize
}

3. 优化useViewport实现(可选,从根源减少多余setState)

现有useViewport单独维护viewport状态属于多余更新,直接根据windowWidth计算返回值即可,减少一次状态流转开销:

import { breakpoints } from "helpers/breakpoints"
import { useWindowSize } from "hooks/useWindowSize"
import { Viewport } from "./types/useViewportTypes"

const useViewport = () => {
  const windowWidth = useWindowSize().width
  const width = windowWidth ?? (typeof window !== 'undefined' ? window.innerWidth : 0)

  if (!width) return undefined

  return {
    isZeroAndUp: width >= 0,
    isMobileAndUp: width >= breakpoints.mobileAndUp,
    isTabletAndUp: width >= breakpoints.tabletAndUp,
    isDesktopAndUp: width >= breakpoints.desktopAndUp,
    isLargeDesktopAndUp: width >= breakpoints.largeDesktopAndUp,
    isMaxWidthAndUp: width >= breakpoints.maxWidthAndUp,
  } as Viewport
}

export default useViewport

验证结果

修复后快速切换路由、频繁挂载卸载使用该Hook的组件,控制台不会再抛出内存泄漏警告,首屏也不会出现viewport值为空的短暂异常。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 18:45:42