React Hook中Apollo useLazyQuery为何被意外调用?
我在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

