Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Skip argument is being ignored in useQuery hook from @apollo/react-hooks

The issue:

Hello, so I've been using apollo-client for a while on my ReactJS application. I've just noticed that sometimes when I use the useQuery hook, the execution completely ignores the skip argument, and just proceed with the onCompleted (albeit without any data). Interestingly enough, it also does not make an API request to my endpoint either. However, if I just set skip to false, then everything works properly (as expected).

Also, this doesn't happen every time I use useQuery with skip, it seems working on some and not others.

Why is apollo ignoring the skip argument and just executing onCompleted immediately with null data? Is there a fix (other than useLazyQuery)?

Code example:

const userQuery = useQuery(GET_USER, {
    variables: {
      value: userId,
    },
    skip: true,
    onCompleted: props => {
      console.log(`props => `, props)
      const {
        user: { type, role, firstName, lastName, permissions },
      } = props;

      setValue('firstName', firstName);
      setValue('lastName', lastName);
      setValue(
        'type',
        userTypes.find(({ value }) => value === type),
      );
      setValue(
        'role',
        userRoles.find(({ value }) => value === role),
      );
    },
  });

Additional notes:

• The output of logging the props from the onCompleted function is props => undefined.

• I added skip: true just to demonstrate the fact that it actually isn't working.

• If I log userQuery itself, then immediately on the first log, userQuery.called is equal to true (but like I said previously, no API call actually had been executed)

Dependencies

"@apollo/react-hooks": "^3.1.5",
"apollo-cache-inmemory": "^1.6.3",
"apollo-client": "^2.6.10",
"apollo-datasource-rest": "^0.6.6",
"apollo-link": "^1.2.13",
"apollo-link-context": "^1.0.19",
"apollo-link-error": "^1.1.12",
"apollo-link-rest": "^0.7.3",
"apollo-link-retry": "^2.2.15",
"apollo-link-token-refresh": "^0.2.7",
"apollo-upload-client": "^11.0.0",
"react": "16.10.2",
"react-apollo": "^3.1.3",

Note:

useLazyQuery seems to be working properly, so as a workaround for this in the meantime you can use that in combination with useEffect to achieve similar results.

like image 917
jengel Avatar asked Nov 15 '22 11:11

jengel


1 Answers

Apollo Client has issues. The bug in skip is an annoying one. You can abandon GraphQL, or work around them. I recommend abandoning it, but if you can't here is a work around. This module will work similar to useQuery and have skip working. It is incomplete to the full spec, but should get you started.

import { useEffect, useState } from 'react';
import { QueryHookOptions, useApolloClient } from '@apollo/react-hooks';
import { ApolloQueryResult, NetworkStatus, QueryOptions, ApolloError } from 'apollo-client';

interface DocumentNode {
    loc: {
        source: {
            body: string;
        };
    };
}

interface QueryResult<T> extends ApolloQueryResult<T> {
    error?: Error | ApolloError;
    refetch?: () => void;
}

/**
 * Method: useQuery
 * Description: enable skip on useQuery. There is a bug that currently ignores it. The bug is confirmed in the 3.2 release as well.
 * Note: https://github.com/apollographql/react-apollo/issues/3492
 */
export function useQuery<T>(
    query: DocumentNode,
    options?: QueryHookOptions
): QueryResult<T | null> {
    // Note: using useApolloClient because useLazyQuery fires on initialization. If we are skipping, we don't even want the first fire.
    const apolloClient = useApolloClient();
    const [result, setQueryResult] = useState<QueryResult<T | null>>({
    // Note: initial state
        data: null,
        loading: false,
        stale: false,
        networkStatus: NetworkStatus.ready
    });

    const setResult = (res: QueryResult<T | null>) => {
    // Note: GraphQL and Apollo can't decide if they want to return errors, or error in the type contract. I want to be sure the contract is stable. If there are errors, there will be error.
    if (res.errors?.length && !res.error) {
        [res.error] = res.errors;
    }
    // Note: Functions should always exist even if the query is not complete.
    if (!res.refetch) {
        res.refetch = () => {
            if (!result.loading) {
                execQuery();
            }
        };
      }
      setQueryResult(res);
  };

  const execQuery = async () => {
      // Note: loading state. Not spreading "...result" to allow errors to be reset.
      setResult({
          data: options?.fetchPolicy === 'no-cache' ? null : result.data,
          loading: true,
          networkStatus: NetworkStatus.loading,
          stale: true
      });
      try {
          const queryOptions = ({
              ...options,
              query
          } as unknown) as QueryOptions;
          
          await apolloClient
              .query(queryOptions)
              .then(result => {
                  setResult(result);
                  if(options?.onCompleted) options.onCompleted(result.data);
              })
              .catch(err => {
                  setResult(err);
                  if(options?.onError) options.onError(err);
              });
          
      } catch (err) {
          // Note: fail state
          setResult({
              ...result,
              loading: false,
              errors: [err],
              error: err,
              stale: true,
              networkStatus: NetworkStatus.error
          });
      }
  };

  useEffect(() => {
      // Note: Skip Functionality
      if (!result.loading && options?.skip !== true) {
           execQuery();
      }
  // Note only listen too explicit variables. If you listen to whole objects they are always different and will cause endless loops.
  }, [options?.skip, query.loc.source.body]);

  return result;
}
like image 190
Mardok Avatar answered Dec 19 '22 17:12

Mardok