Skip to content

React 轮询 Hook 使用说明

源码

ts
import { useRef, useEffect, useState, useCallback } from "react";

/**
 * 轮询 Hook 配置项接口
 */
interface UsePollingOptions<T = unknown> {
  /** 轮询函数,返回 Promise */
  pollingFn: () => Promise<T>;
  /** 轮询间隔(毫秒),默认 1000ms */
  interval?: number;
  /** 最大轮询次数,默认 100 次 */
  maxCount?: number;
  /** 是否组件挂载后立即执行,默认 true */
  immediate?: boolean;
  /** 轮询条件函数,返回 false 时停止轮询 */
  condition?: () => boolean;
  /** 轮询成功回调 */
  onSuccess?: (data: T) => void;
  /** 轮询失败回调 */
  onError?: (error: unknown) => void;
  /** 轮询完成回调(达到最大次数或条件不满足时) */
  onComplete?: () => void;
}

/**
 * 通用轮询 Hook(React 版)
 * @param options 轮询配置项
 * @returns 轮询相关状态和操作方法
 */
export function usePolling<T = unknown>(options: UsePollingOptions<T>) {
  const {
    pollingFn,
    interval = 1000,
    maxCount = 100,
    immediate = true,
    condition,
    onSuccess,
    onError,
    onComplete,
  } = options;

  // 是否正在轮询
  const [isPolling, setIsPolling] = useState(false);
  // 是否暂停轮询
  const [isPaused, setIsPaused] = useState(false);
  // 当前轮询次数(已移除 useState)
  // const [count, setCount] = useState(0);
  // 只用 countRef 追踪轮询次数
  const countRef = useRef(0);
  // 返回的数据
  const [data, setData] = useState<T | null>(null);
  // 轮询错误
  const [error, setError] = useState<unknown>(null);
  // 定时器引用
  const timerRef = useRef<NodeJS.Timeout | null>(null);

  /**
   * 轮询主逻辑
   */
  const polling = async () => {
    // console.log("pollingFn22", isPaused, maxCount, countRef.current);

    // 如果已暂停则不执行
    if (isPaused) return;
    // 条件不满足时停止轮询
    if (condition && !condition()) {
      stopPolling();
      onComplete?.();
      return;
    }
    // 达到最大轮询次数时停止轮询
    if (countRef.current >= maxCount) {
      stopPolling();
      onComplete?.();
      return;
    }
    try {
      countRef.current += 1;
      const result = await pollingFn();
      setData(result);
      setError(null);
      onSuccess?.(result);
    } catch (err) {
      setError(err);
      onError?.(err);
    }
  };

  /**
   * 开始轮询
   */
  const startPolling = () => {
    console.log("startPolling", isPolling);
    if (isPolling) return;
    setIsPolling(true);
    setIsPaused(false);
    countRef.current = 0; // 重置 ref
    setError(null);
    // 定时执行 polling
    timerRef.current = setInterval(() => {
      polling();
    }, interval);
  };

  /**
   * 停止轮询
   */
  const stopPolling = useCallback(() => {
    setIsPolling(false);
    setIsPaused(false);
    if (timerRef.current) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
  }, []);

  /**
   * 重置轮询(停止后重新开始)
   */
  const resetPolling = useCallback(() => {
    stopPolling();
    countRef.current = 0; // 重置 ref
    setData(null);
    setError(null);
    startPolling();
  }, [stopPolling, startPolling]);

  /**
   * 暂停轮询
   */
  const pausePolling = useCallback(() => {
    setIsPaused(true);
  }, []);

  /**
   * 恢复轮询
   */
  const resumePolling = useCallback(() => {
    setIsPaused(false);
  }, []);

  /**
   * 组件挂载时根据 immediate 自动开始轮询,卸载时自动清理
   */
  useEffect(() => {
    if (immediate) {
      startPolling();
    }
    return () => {
      stopPolling();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // 返回所有状态和操作方法,便于外部调用
  return {
    isPolling, // 是否正在轮询
    isPaused, // 是否暂停
    count: countRef.current, // 当前轮询次数(直接返回 ref 的值)
    data, // 轮询数据
    error, // 轮询错误
    startPolling, // 开始轮询
    stopPolling, // 停止轮询
    resetPolling, // 重置轮询
    pausePolling, // 暂停轮询
    resumePolling, // 恢复轮询
  };
}

使用

ts
import { usePolling } from "./usePolling.ts";

// usePolling 初始化 immediate 固定为 false
const { startPolling } = usePolling({
  pollingFn: fetchBackends,
  interval: 1000,
  immediate: false, // 固定为 false
  maxCount: 100,
  condition: () => {
    return (
      backendsRef.current && backendsRef.current?.predict_all_task === true
    );
  },
  onComplete: () => {},
});

// 监听 backends 变化
useEffect(() => {
  if (backends?.predict_all_task) {
    startPolling();
  }
}, [backends, startPolling]);

const fetchBackends = useCallback(async () => {
  const models = await api.callApi("mlBackends", {
    params: {
      project: project.id,
      include_static: true,
    },
  });
  if (models) {
    setBackends(models[0]);
  }
}, [api, project]);

vue3 同样版本的

ts
import { useEffect, useRef, useState, useCallback } from "react";
import {
  createPolling,
  type IPollingOptions,
  type PollingManager,
} from "./polling";

/**
 * React轮询Hook
 * @param options 轮询配置选项
 * @returns 轮询相关的状态和方法
 */
export function usePolling<T = unknown>(options: IPollingOptions<T>) {
  const [isPolling, setIsPolling] = useState(false);
  const [isPaused, setIsPaused] = useState(false);
  const [count, setCount] = useState(0);
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<unknown>(null);

  const pollingManagerRef = useRef<PollingManager<T> | null>(null);
  const syncTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const latestOptionsRef = useRef(options);

  // 始终持有最新的回调与条件(避免闭包陈旧)
  useEffect(() => {
    latestOptionsRef.current = options;
  }, [
    options.pollingFn,
    options.condition,
    options.onSuccess,
    options.onError,
    options.onComplete,
    options.onStatus,
  ]);

  // 创建轮询管理器
  const createManager = useCallback(() => {
    const base = latestOptionsRef.current;
    const manager = createPolling<T>({
      interval: base.interval,
      maxCount: base.maxCount,
      immediate: base.immediate,
      pollingFn: async () => {
        return await latestOptionsRef.current.pollingFn();
      },
      condition: () => {
        return latestOptionsRef.current.condition?.() ?? true;
      },
      onSuccess: (result: T) => {
        setData(result);
        setError(null);
        latestOptionsRef.current.onSuccess?.(result);
      },
      onError: (err: unknown) => {
        setError(err);
        latestOptionsRef.current.onError?.(err);
      },
      onComplete: () => {
        latestOptionsRef.current.onComplete?.();
      },
      onStatus: (status) => {
        latestOptionsRef.current.onStatus?.(status);
      },
    });

    // 监听状态变化
    const updateState = () => {
      if (manager) {
        setIsPolling(manager.isPolling);
        setIsPaused(manager.isPaused);
        setCount(manager.count);
        setData(manager.data);
        setError(manager.error);
      }
    };

    // 创建定时器来同步状态
    syncTimerRef.current = setInterval(updateState, 100);

    // 保存引用
    pollingManagerRef.current = manager;

    return manager;
  }, []);

  // 控制方法
  const startPolling = useCallback(() => {
    if (!pollingManagerRef.current) {
      createManager();
    }
    pollingManagerRef.current?.startPolling();
  }, [createManager]);

  const stopPolling = useCallback(() => {
    pollingManagerRef.current?.stopPolling();
  }, []);

  const resetPolling = useCallback(() => {
    pollingManagerRef.current?.resetPolling();
  }, []);

  const pausePolling = useCallback(() => {
    pollingManagerRef.current?.pausePolling();
  }, []);

  const resumePolling = useCallback(() => {
    pollingManagerRef.current?.resumePolling();
  }, []);

  // 组件卸载时清理
  useEffect(() => {
    return () => {
      if (syncTimerRef.current) {
        clearInterval(syncTimerRef.current);
      }
      if (pollingManagerRef.current) {
        pollingManagerRef.current.destroy();
        pollingManagerRef.current = null;
      }
    };
  }, []);

  // 配置变更时(interval/maxCount/immediate),若当前未在轮询,重建管理器以生效
  useEffect(() => {
    const manager = pollingManagerRef.current;
    if (!manager) return;
    if (manager.isPolling) return; // 运行中不热更新,避免状态跳变
    // 先清理
    if (syncTimerRef.current) {
      clearInterval(syncTimerRef.current);
      syncTimerRef.current = null;
    }
    manager.destroy();
    pollingManagerRef.current = null;
    createManager();
  }, [options.interval, options.maxCount, options.immediate, createManager]);

  return {
    isPolling,
    isPaused,
    count,
    data,
    error,
    startPolling,
    stopPolling,
    resetPolling,
    pausePolling,
    resumePolling,
    // 手动创建管理器的方法(用于延迟创建)
    createManager,
  };
}

Released under the ISC License.