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

React useInterval调用Hook更新状态触发无限重渲染问题咨询

问题解答

1. 代码冗余性评估

当前实现确实存在大量冗余代码,同时还违反了React Hook的基本调用规则:

  • CallProjectScans、GetProjectScans两个函数属于无意义的重复包装,本质都是对useFetchProjectScansByUser调用的简单封装,没有实现任何额外逻辑,反而把Hook调用放到了普通嵌套函数里,违反Hook调用规则。
  • 组件内定义的stateProjectScans、stateIsProjectScansFetchComplete两个状态是对自定义Hook内部状态的冗余复制。自定义Hook本身已经维护了这两个状态并对外返回,父组件完全可以直接使用返回值,不需要额外存一份再做同步,平白增加状态同步成本和bug风险。
  • 代码里加的@ts-ignore也是冗余的,只要类型定义正确根本不需要忽略类型检查。

2. 30秒轮询更新的更优实现方案

你现在的写法核心错误是把React Hook当成普通异步函数在定时器、useEffect回调里调用,这违反了Hook只能在组件/自定义Hook顶层调用的硬性规则,也是出现bug的核心原因。
最优方案是把轮询逻辑直接封装到自定义Hook内部,父组件不需要维护重复状态、不需要自己管理定时器,直接拿Hook返回值使用即可,参考实现如下:

第一步:改造支持轮询的自定义Hook

export const useFetchProjectScansByUser = (
  user: string | null,
  pollInterval: number = 0 // 传入毫秒数即开启轮询,传0默认不轮询
): [IProjectScan[], boolean] => {
  const [projectScans, setProjectScans] = useState<IProjectScan[]>([]);
  const [isProjectScansFetchComplete, setIsProjectScansFetchComplete] = useState<boolean>(false);
  const { enqueueSnackbar } = useSnackbar();
  // 用ref存定时器ID,方便卸载时清理
  const timerRef = useRef<number | null>(null);

  // 抽离拉取逻辑为稳定引用的回调
  const fetchProjectScans = useCallback(async () => {
    try {
      const response = await fetch(/* 你的请求URL */);
      if (!response.ok) throw response.statusText;
      const json = await response.json();
      const scanList = createProjectScansFromJSON(json);
      setProjectScans(scanList);
    } catch (err) {
      // 原有错误处理逻辑,比如snackbar提示
    } finally {
      setIsProjectScansFetchComplete(true);
      // 配置了轮询间隔则安排下一次请求
      if (pollInterval > 0) {
        timerRef.current = window.setTimeout(fetchProjectScans, pollInterval);
      }
    }
  }, [user, pollInterval, enqueueSnackbar]);

  useEffect(() => {
    // 首次挂载立即拉取
    fetchProjectScans();
    // 组件卸载时清理定时器,防止内存泄漏
    return () => {
      if (timerRef.current) clearTimeout(timerRef.current);
    };
  }, [fetchProjectScans]);

  return [projectScans, isProjectScansFetchComplete];
};

第二步:简化父组件代码

改造后父组件不需要任何冗余包装函数、不需要重复维护状态,代码可以简化为:

const ExecutionsPage: React.FC = () => {
  const user = sessionStorage.getItem('user');
  const UPDATE_TABLE_TIMER = 30000;
  // 直接使用Hook返回值,自动处理首次拉取+30秒轮询
  const [projectScans, isProjectScansFetchComplete] = useFetchProjectScansByUser(
    user,
    UPDATE_TABLE_TIMER
  );

  // 直接使用projectScans和isProjectScansFetchComplete渲染页面即可
  return <>{/* 你的页面JSX */}</>;
};

这种写法把数据拉取、轮询、状态维护的逻辑全部收敛到自定义Hook内部,父组件只需要拿值渲染,可维护性和可读性都远高于原有实现。

3. 无限重渲染的具体成因

你遇到的死循环是多个问题叠加导致的:

  • 直接触发点:useInterval传参错误。你写的useInterval(GetProjectScans(), UPDATE_TABLE_TIMER)是把GetProjectScans()的执行结果传给了useInterval,而不是把函数引用传进去。这意味着组件每次渲染到这一行,都会立刻执行GetProjectScans函数,函数内部调用setStateProjectScans触发组件重渲染,重渲染又会再次执行这行代码,直接形成「渲染→执行setState→重渲染」的死循环。
  • Hook调用规则被破坏。你在普通嵌套函数、useEffect回调里调用useFetchProjectScansByUser,React无法正确追踪Hook的状态绑定关系,每次执行这些函数时Hook内部的状态都会出现不可预期的更新,反复触发重渲染。
  • 重复状态同步放大问题。你在父组件额外维护了一份和Hook返回值重复的状态,每次Hook内部更新触发父组件重渲染时,你又会执行同步逻辑更新父组件自己的state,再次触发重渲染,进一步加剧了循环。

内容的提问来源于stack exchange,提问作者web-dev-nerd199

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 01:51:45