React自定义轮询Hook中useEffect内while循环无法终止问题求解
问题根本原因
你遇到的无法终止循环的问题由两个核心原因导致:
- 闭包变量捕获问题:useEffect中执行的async函数在初始化时捕获了当时的
condition状态值true,后续调用setCondition(false)修改的是React状态,只会触发组件重渲染,已经在运行的while循环持有的始终是初始化时的旧condition值,无法感知到状态变化。 - 多轮询实例并行问题:你将
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
相关产品推荐
相关产品推荐

