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

React自定义轮询Hook中useEffect内while循环无法终止问题求解

问题根本原因

你遇到的无法终止循环的问题由两个核心原因导致:

  1. 闭包变量捕获问题:useEffect中执行的async函数在初始化时捕获了当时的condition状态值true,后续调用setCondition(false)修改的是React状态,只会触发组件重渲染,已经在运行的while循环持有的始终是初始化时的旧condition值,无法感知到状态变化。
  2. 多轮询实例并行问题:你将condition加入了useEffect的依赖数组,每次condition变化都会重新执行effect,生成新的while循环实例,旧的循环还未退出,就会出现多个轮询同时运行的异常。

修复方案(无需使用setInterval)

核心修改是将轮询控制标志从React state改为useRef存储,ref的current属性是可变引用,不会存在闭包捕获旧值的问题,修改也不会触发不必要的重渲染。

修改后的usePolling.ts代码

/* eslint-disable no-await-in-loop */
import { useState, useCallback, useEffect, useRef } from "react";

export interface PollingOptions<T> {
  fetchFunc: () => Promise<T> | undefined;
  // 可选配置:是否立即触发第一次请求,默认true
  immediate?: boolean;
  // 可选配置:最大重试次数,默认不限制
  maxRetries?: number;
}

export const usePolling = <T>(
  { fetchFunc, immediate = true, maxRetries = Infinity }: PollingOptions<T>,
  interval: number
) => {
  const [data, setData] = useState<T>();
  const [error, setError] = useState<Error>();
  const isPollingRef = useRef(true);
  const retryCountRef = useRef(0);

  const stopPolling = useCallback(() => {
    isPollingRef.current = false;
  }, []);

  const performPolling = useCallback(async () => {
    if (retryCountRef.current >= maxRetries) {
      stopPolling();
      setError(new Error("轮询已达到最大重试次数"));
      return;
    }
    try {
      const res = await fetchFunc?.();
      setData(res);
      retryCountRef.current = 0; // 请求成功重置重试计数
    } catch (err) {
      retryCountRef.current += 1;
      if (retryCountRef.current >= maxRetries) {
        stopPolling();
        setError(err as Error);
      }
    } finally {
      if (isPollingRef.current) {
        await new Promise((r) => setTimeout(r, interval));
      }
    }
  }, [fetchFunc, interval, maxRetries, stopPolling]);

  // 手动触发一次轮询的方法,适用主动刷新场景
  const triggerOnce = useCallback(async () => {
    await performPolling();
  }, [performPolling]);

  useEffect(() => {
    isPollingRef.current = true;
    retryCountRef.current = 0;

    (async () => {
      if (!immediate) {
        await new Promise((r) => setTimeout(r, interval));
      }
      while (isPollingRef.current) {
        await performPolling();
      }
    })();

    return () => {
      isPollingRef.current = false;
    };
  }, [performPolling, immediate]);

  return { data, error, stopPolling, triggerOnce };
};

调整说明

  • 原有App.tsx不需要做任何修改即可正常运行,调用stopPolling后会立刻终止while循环。
  • 新增了可选配置项immediate控制是否跳过初始等待直接发起第一次请求,maxRetries控制最大重试次数,避免异常场景下无限轮询。
  • 新增了triggerOnce方法,支持业务侧手动触发一次请求,适用主动刷新的业务场景。
  • 移除了useEffect中不必要的依赖项,避免重复执行生成多个轮询实例,组件卸载时会自动终止轮询,不会产生内存泄漏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 13:27:04