javascriptreactjsnext.jsgraphqlapollo-client

Why is my Apollo useLazyQuery being called unexpectedly in a React hook?


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;

If you have a close look that i place some console log to check what's going on. The problem is fetchUserSession is being invoked although it's not been called from the component. I ensure that components are being re-rendered? I see nothing that re-rendered. In fact, i put some console log in that blocks where fetchUserSession is invoked from those components. In the custom hook i used useLazyQuery hook instead useQueryand the way i've coded it'd invoke manually but at some point it's being called but it should not be.

What's wrong with my code? Or, is this actually an apollo-client-end issue?

I'm using "@apollo/client": "^3.7.16".


Solution

  • useQuery and useLazyQuery has some strange rules on when it gets called. The more I use Apollo, the more I avoid useLazyQuery. UseQuery seems to be more stable, but it can still get called randomly even if it was supposed to get called once with skip property.

    Basically, state changes (Sometimes) or context state changes (Most times) can trigger both useLazyQuery and useQuery even if nothing has changed in their variables. I think that if they have no variables inside of them, they get called even more times on state changes.

    The solution I use is to use the apolloClient to call it directly. However, they have no onError or onCompleted callback, and you have to manually control the data, loading and error state.

    //...Component
      const apolloClient = useApolloClient(); //Hook
    
      const myFunction = async () => {
       try {
         const { data, loading, error } = await 
            apolloClient.query({
                query: ALL_PRODUCT_SKUS,
                //...
              });
       } catch(err) {
      //...
     }
    }
    

    If you still wish to use the hooks, then try to avoid state changes or global context changes, as they are the reason for this issue.

    Another reason why they might be called multiple times is if you are clearing apollo cache. One of the methods will recall all of active queries, the other one will not.

    If you are setting multiple async state changes in one go, each state set will call the useLazyQuery/ useQuery again. So 5 times in below example. (Might not be true anymore in newer versions of react)

    const myFunction = async () => {
    setStateA(true)
    setStateB(true)
    setStateC(true)
    setStateD(true)
    setStateF(true)
    }
    

    If you use react-native, it gets even worse because state/context state changes in top stacks can call queries from the screens on the bottom stack