自定义React Hook中setTimeout未正确清除多次触发问题排查
问题原因
你代码里有两个核心错误导致定时器多次触发:
错误引入了Node.js专属的定时器
自定义Hook里的import { setTimeout } from 'timers';是Node.js环境的模块导入语句,浏览器环境不存在这个模块的实现,导入后会导致定时器的清理逻辑完全失效,你可以直接使用浏览器全局挂载的setTimeout、clearTimeout,不需要额外导入。useEffect依赖项配置错误
你把screenWidth加到了useEffect的依赖数组中,导致每次screenWidth更新时,useEffect都会重新执行:首先执行上一轮effect的清理函数移除旧的resize监听,再注册新的resize监听。而上一轮effect闭包中创建的未执行定时器,在本轮effect中完全访问不到,你的clearTimeout只能清掉当前闭包内的定时器,历史遗留的定时器就会正常触发,最终表现为回调多次执行。
你之前写在组件内的逻辑实际上也有同样的问题,只是因为你设置的延时只有100ms,触发频率低不容易感知到而已。
修复方案
修改后的自定义Hook代码如下:
import { useEffect, useState, useRef } from 'react'; export const useDebounceScreenWidth = (delay = 1000) => { const [screenWidth, setScreenWidth] = useState(window.innerWidth); // 用useRef存储定时器ID,保证所有闭包都能访问到同一个引用 const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null); useEffect(() => { const listener = () => { if (timerRef.current) clearTimeout(timerRef.current); timerRef.current = setTimeout(() => { setScreenWidth(window.innerWidth); console.log(screenWidth); }, delay); }; window.addEventListener('resize', listener); // 初始调用一次获取正确的初始宽度 listener(); return () => { window.removeEventListener('resize', listener); // 组件卸载时清理剩余定时器 if (timerRef.current) clearTimeout(timerRef.current); }; // 依赖项设为空数组,只在Hook初始化时执行一次 }, []); return [screenWidth] as const; };
修改点说明:
- 移除了错误的Node.js定时器导入
- useEffect依赖改为空数组,仅注册一次监听事件
- 用
useRef存储定时器ID,保证所有闭包都能访问到同一个定时器引用,不会出现清不掉历史定时器的问题 - 增加了初始调用和卸载时的定时器清理逻辑,避免内存泄漏
- 给返回值加了
as const类型声明,调用时可以正确推导screenWidth是number类型,不会被识别为(number | void)[]
内容的提问来源于stack exchange,提问作者Oskar Zaremba
相关产品推荐
相关产品推荐

