React JS自定义Timeout Hook无法清除全部实例的问题排查
自定义Hook超时功能问题排查与修复
我编写了以下代码实现一个处理超时功能的自定义Hook,预期点击按钮后显示消息,并在一段时间后自动清除。但点击「Open top message」按钮3次后,只有最后一条消息会消失,其余消息无法自动移除。
import "./styles.css"; import React, { useRef, useCallback, useState, useEffect } from "react"; const useTimer = () => { const timer = useRef(); const fn = useCallback((callback, timeout = 0) => { clearTimeout(timer.current); timer.current = setTimeout(() => { callback(); }, timeout); }, []); const clearTimeoutHandler = () => { console.log("clear"); return clearTimeout(timer.current); }; useEffect(() => { return clearTimeoutHandler(); }, []); return { fn, clearTimeoutHandler }; }; export default function App() { const [state, setState] = useState(false); const [list, updateList] = useState([]); const timer = useTimer(); const removeItem = (id) => { updateList((list) => list.filter((x) => x.id !== id)); timer.clearTimeoutHandler(); }; const onClickHandler = () => { const id = new Date(); const element = { id: id, text: "text" + new Date() }; updateList(list.concat([{ ...list, ...element }])); timer.fn(() => { removeItem(id); }, 2000); }; const bottomMess = () => { setState({ m: "hi" }); timer.fn( () => { setState(undefined); }, 2000 ); }; return ( <div className="App"> <button onClick={bottomMess}>Open bottom message</button> <button onClick={onClickHandler}>Open top message</button> {list.map((d) => ( <h1 key={d.id}>{d.text}</h1> ))} {state && ( <div style={{ position: "absolute", bottom: 0 }}> bottom message {state.m} </div> )} </div> ); }
问题分析
当前useTimer Hook存在核心问题:
- 仅使用单个
timerref存储定时器ID,每次调用fn时都会执行clearTimeout(timer.current),直接清除上一次创建的定时器。连续点击按钮时,前两次的定时器还未触发就被第三次调用的fn清除,导致只有最后一条消息的定时器能正常执行。 removeItem中调用timer.clearTimeoutHandler()会清除当前唯一的定时器,即使还有其他消息的定时器未触发,也会被提前清除,进一步导致消息无法自动消失。- 额外问题:
updateList(list.concat([{ ...list, ...element }]))写法错误,会把list数组的属性合并到element中,正确应该直接添加element对象。
修复方案
修改useTimer Hook,让它支持存储多个独立的定时器,避免互相覆盖。用Map来存储每个定时器的ID(关联消息ID),同时提供添加、清除单个定时器和清除全部定时器的方法。
修复后的代码
自定义Hook useTimer
const useTimer = () => { const timers = useRef(new Map()); // 添加定时器,返回用于清除该定时器的方法 const setTimer = useCallback((id, callback, timeout = 0) => { // 先清除该ID对应的旧定时器(如果存在) if (timers.current.has(id)) { clearTimeout(timers.current.get(id)); } const timerId = setTimeout(() => { callback(); timers.current.delete(id); }, timeout); timers.current.set(id, timerId); // 返回清除当前定时器的方法 return () => { clearTimeout(timerId); timers.current.delete(id); }; }, []); // 清除单个定时器 const clearTimer = useCallback((id) => { if (timers.current.has(id)) { clearTimeout(timers.current.get(id)); timers.current.delete(id); } }, []); // 清除所有定时器 const clearAllTimers = useCallback(() => { timers.current.forEach(timerId => clearTimeout(timerId)); timers.current.clear(); }, []); // 组件卸载时清除所有定时器 useEffect(() => { return clearAllTimers; }, [clearAllTimers]); return { setTimer, clearTimer, clearAllTimers }; };
修改后的App组件
export default function App() { const [state, setState] = useState(false); const [list, updateList] = useState([]); const { setTimer, clearTimer } = useTimer(); const removeItem = (id) => { updateList((prevList) => prevList.filter((x) => x.id !== id)); // 清除当前消息对应的定时器 clearTimer(id); }; const onClickHandler = () => { const id = Date.now(); // 用时间戳作为唯一ID,比new Date()更方便 const element = { id: id, text: "text" + new Date().toLocaleString() }; // 正确添加新元素到列表 updateList(prevList => [...prevList, element]); // 设置定时器,关联当前消息ID setTimer(id, () => { removeItem(id); }, 2000); }; const bottomMess = () => { setState({ m: "hi" }); // 底部消息用固定ID区分 setTimer('bottomMsg', () => { setState(undefined); }, 2000); }; return ( <div className="App"> <button onClick={bottomMess}>Open bottom message</button> <button onClick={onClickHandler}>Open top message</button> {/* 给列表项添加key属性,符合React要求 */} {list.map((d) => ( <h1 key={d.id}>{d.text}</h1> ))} {state && ( <div style={{ position: "absolute", bottom: 0 }}> bottom message {state.m} </div> )} </div> ); }
修复说明
useTimer改用Map存储多个定时器,每个定时器通过唯一ID(消息ID或自定义标识)关联,避免互相覆盖。setTimer方法会先清除同一ID的旧定时器(防止重复创建),然后存储新的定时器ID,并返回清除该定时器的方法。removeItem仅清除当前消息对应的定时器,不会影响其他消息的定时任务。- 修复了列表更新的错误写法,同时给列表项添加了
key属性,符合React的渲染规范。 - 底部消息使用固定ID
'bottomMsg',确保再次点击时会清除上一次的定时器,避免多个底部消息叠加。
内容的提问来源于stack exchange,提问作者Asking
相关产品推荐
相关产品推荐

