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

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存在核心问题:

  1. 仅使用单个timer ref存储定时器ID,每次调用fn时都会执行clearTimeout(timer.current),直接清除上一次创建的定时器。连续点击按钮时,前两次的定时器还未触发就被第三次调用的fn清除,导致只有最后一条消息的定时器能正常执行。
  2. removeItem中调用timer.clearTimeoutHandler()会清除当前唯一的定时器,即使还有其他消息的定时器未触发,也会被提前清除,进一步导致消息无法自动消失。
  3. 额外问题: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>
  );
}

修复说明

  1. useTimer改用Map存储多个定时器,每个定时器通过唯一ID(消息ID或自定义标识)关联,避免互相覆盖。
  2. setTimer方法会先清除同一ID的旧定时器(防止重复创建),然后存储新的定时器ID,并返回清除该定时器的方法。
  3. removeItem仅清除当前消息对应的定时器,不会影响其他消息的定时任务。
  4. 修复了列表更新的错误写法,同时给列表项添加了key属性,符合React的渲染规范。
  5. 底部消息使用固定ID'bottomMsg',确保再次点击时会清除上一次的定时器,避免多个底部消息叠加。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 13:45:40