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

React Hook中Apollo useLazyQuery为何被意外调用?

React自定义Hook中useLazyQuery的fetchUserSession意外触发问题

我在React自定义Hook中使用Apollo的useLazyQuery时,碰到了fetchUserSession被意外调用的问题。已经确认组件没有重渲染(在组件调用fetchUserSession的位置添加了日志,无触发记录),而useLazyQuery本应仅在手动调用时执行。使用的@apollo/client版本为^3.7.16。

相关代码如下:

import { useLazyQuery } from '@apollo/client';
import { useEffect, useState } from 'react';

import {
  ContestSessionResponseInfoObject,
  GetSessionDocument,
  HasAccessToRoundDocument,
} from '@/graphql/generated/shikho-private-hooks';
import useQuizUserSessionStore from '@/store/userSessionStore';
import { calcTotalSec, isDateExpired } from '@/utils/helpers';

const useUserSession = (roundId: string, userId = '' as string) => {
  const [isLoading, setIsLoading] = useState(false);

  const {
    setUserSession,
    userSession,
    setUserSessionExpired,
    sessionExpired,
    expiryTime,
    setExpiryTime,
    setHasAccessToRound,
    hasAccessToRound,
  } = useQuizUserSessionStore();

  const updateSessionState = (data: ContestSessionResponseInfoObject) => {
    const expired = isDateExpired(data?.expiry_time ?? '');
    if (data) setUserSession(data);
    if (data?.is_final_submitted === false && expired)
      setUserSessionExpired(expired);

    if (data?.expiry_time) setExpiryTime(calcTotalSec(data?.expiry_time ?? ''));
  };

  const [fetchUserSession] = useLazyQuery(GetSessionDocument, {
    variables: { round_id: roundId },
    fetchPolicy: 'network-only',
    /* eslint-disable @typescript-eslint/ban-ts-comment */
    // @ts-ignore
    onCompleted: ({ getSession: { session } = {} }) => {
      console.log('Query completed');
      updateSessionState(session);
      setIsLoading(false);
    },
  });
  const [getHasAccessToRound] = useLazyQuery(HasAccessToRoundDocument, {
    variables: { round_id: roundId, user_id: userId },
    onCompleted: (data) => {
      setHasAccessToRound(!!data?.hasAccessToRound?.has_access);
    },
  });

  useEffect(() => {
    if (roundId.length && !userSession && !isLoading) {
      setIsLoading(true);
      console.log('Inside hook');
      fetchUserSession();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [setUserSession, roundId]);

  useEffect(() => {
    if (roundId && userId) getHasAccessToRound();
  }, [getHasAccessToRound, setUserSession, roundId, userId]);

  return {
    userSession,
    sessionExpired,
    updateSessionState,
    expiryTime,
    fetchUserSession,
    hasAccessToRound,
  };
};

export default useUserSession;

问题分析与解决方案

1. 核心问题:useEffect依赖数组不完整

第一个useEffect禁用了exhaustive-deps规则,导致依赖数组缺失了userSession和isLoading两个关键变量。这会引发两种问题:

  • 当userSession被其他逻辑清空(比如store状态变更),或者isLoading状态变化时,effect无法感知到这些变化,可能在不符合预期的时机触发。
  • 由于React的effect依赖检查机制,缺失的依赖可能导致effect在组件/Hook重新渲染时意外执行,进而调用fetchUserSession。

修复方式:
移除eslint-disable注释,补充完整的依赖数组:

useEffect(() => {
  if (roundId.length && !userSession && !isLoading) {
    setIsLoading(true);
    console.log('Inside hook');
    fetchUserSession();
  }
}, [roundId, userSession, isLoading, setUserSession]);

2. 排查store状态的意外变更

userSession来自useQuizUserSessionStore,如果其他组件或逻辑意外修改了这个状态(比如调用setUserSession清空数据),会导致!userSession的条件再次满足,触发effect里的fetchUserSession。

排查方式:
在setUserSession的调用位置添加日志,追踪是否有非预期的状态更新;也可以在updateSessionState函数里增加日志,确认每次状态更新的来源。

3. 避免useLazyQuery实例的不必要重建

useLazyQuery返回的查询函数(如fetchUserSession)会在配置项变化时重新生成实例。如果roundId频繁变化,会导致查询函数重建,不过这不会直接触发查询,但如果effect依赖了这个函数,可能导致effect重复执行。

优化方式:
确保useLazyQuery的配置项稳定,比如如果variables依赖的参数有频繁变化的情况,可以用useMemo包裹配置对象:

const sessionQueryOptions = useMemo(() => ({
  variables: { round_id: roundId },
  fetchPolicy: 'network-only' as const,
  onCompleted: ({ getSession: { session } = {} }) => {
    console.log('Query completed');
    updateSessionState(session);
    setIsLoading(false);
  },
}), [roundId, updateSessionState]);

const [fetchUserSession] = useLazyQuery(GetSessionDocument, sessionQueryOptions);

4. 添加重复调用防护

如果需要严格避免fetchUserSession被重复调用,可以用useRef添加一个加载状态标志:

const isSessionFetched = useRef(false);

useEffect(() => {
  if (roundId.length && !userSession && !isLoading && !isSessionFetched.current) {
    isSessionFetched.current = true;
    setIsLoading(true);
    console.log('Inside hook');
    fetchUserSession();
  }
}, [roundId, userSession, isLoading, setUserSession]);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 20:32:55