实现 useTimeout Hook

在 React 函数组件中正确使用 setTimeout 的自定义 Hook

问题

在 React 函数组件中直接使用 setTimeout 会遇到两个问题:

1. 重复创建定时器

每次组件重新渲染都会创建新的定时器,导致定时器不断累积。

function App() {  
  const [state, setState] = useState(1);  
  
  setTimeout(() => {  
    setState(state + 1);  
  }, 3000);  
  
  return <div>{state}</div>;
  // 问题:state 更新后组件重新渲染,又创建新的定时器,变成每 3 秒 +1
}

2. 闭包陷阱

定时器回调函数捕获的是旧的状态值,无法获取最新状态。

function App() {  
  const [count, setCount] = useState(0);
  const [countTimeout, setCountTimeout] = useState(0);
  
  useEffect(() => {  
    setTimeout(() => {  
      setCountTimeout(count); // count 始终是 0
    }, 3000);
    setCount(5);
  }, []);
  
  return <div>{countTimeout}</div>;
  // 问题:3 秒后 countTimeout 是 0,而不是 5
}

解答

使用 useRef 保存最新的回调函数,配合 useEffect 管理定时器生命周期:

function useTimeout(callback, delay) {
  const memorizeCallback = useRef();

  // 每次 callback 更新时,保存最新的回调函数
  useEffect(() => {
    memorizeCallback.current = callback;
  }, [callback]);

  // 创建定时器,组件卸载时清除
  useEffect(() => {
    if (delay !== null) {
      const timer = setTimeout(() => {
        memorizeCallback.current();
      }, delay);
      
      return () => {
        clearTimeout(timer);
      };
    }
  }, [delay]);
}

使用示例

function App() {
  const [count, setCount] = useState(0);

  useTimeout(() => {
    console.log('当前 count:', count);
    setCount(count + 1);
  }, 3000);

  return <div>{count}</div>;
}

关键点

  • 使用 useRef 存储回调函数,避免闭包陷阱,始终执行最新的回调
  • 通过 useEffect 的清理函数 clearTimeout 防止内存泄漏
  • delay 作为依赖项,支持动态修改延迟时间
  • 传入 null 作为 delay 可以暂停定时器