apollo-client icon indicating copy to clipboard operation
apollo-client copied to clipboard

useLazyQuery automatically triggers after being triggered once

Open madhugod opened this issue 3 years ago • 16 comments

Hello, I'm trying to use useLazyQuery to trigger a query on click on a button.

Intended outcome:

It only triggers the query when I click on the button (execQuery())

Actual outcome:

After clicking once on the button, it triggers the query automatically when the input changes (value)

How to reproduce the issue:

function Component() {
  const [value, setValue] = useState('');
  const [execQuery, { loading, data, error }] = useLazyQuery(SEGMENT_QUERY, {
    variables: { value } },
  });

  return (
    <>
      <input type="text" value={value} onChange={(e) => setValue(e.target.value)} />
      <button onClick={() => execQuery()}>dewit</button>
      {loading && 'loading'}
      {error && 'error'}
      <pre>{data && JSON.stringify(data, null, 2)}</pre>
    </>
  );
}

Using the example above:

  • Type some text in the text input, you can see in the Network tab of developer tools that no query is triggered (this is expected).
  • Click on the button, the query is correctly triggered (this is expected)
  • Now go back to typing in the text input, and you can see in the Network tab that a query is triggered on each key stroke (this is unexpected: the query should be triggered only when the button is clicked)

Also I tried to use useQuery with skip: true and refetch, but calling refetch has no effect

Versions

$ npx envinfo@latest --preset apollo --clipboard

System: OS: Windows 10 10.0.19042 Binaries: Node: 14.4.0 - C:\Program Files\nodejs\node.EXE Yarn: 1.22.10 - ~\AppData\Roaming\npm\yarn.CMD npm: 7.24.1 - C:\Program Files\nodejs\npm.CMD Browsers: Chrome: 97.0.4692.71 Edge: Spartan (44.19041.1266.0), Chromium (97.0.1072.62) npmPackages: apollo-server-core: ^3.5.0 => 3.5.0 apollo-server-express: ^3.5.0 => 3.5.0

madhugod avatar Jan 17 '22 15:01 madhugod

I wrote this hook as a temporary workaround

// useMyLazyQuery.ts

import { ApolloQueryResult, OperationVariables, QueryOptions, TypedDocumentNode, useApolloClient } from '@apollo/client';
import { useCallback, useRef, useState } from 'react';

export type MyLazyQueryResult<T> = Omit<ApolloQueryResult<T>, 'networkStatus' | 'data'> & { data: T | undefined };

export default function useMyLazyQuery<T = any, TVariables = OperationVariables>(
  query: TypedDocumentNode<T, TVariables>,
  options: Omit<QueryOptions<TVariables, T>, 'query'>
): [() => Promise<void>, MyLazyQueryResult<T>] {
  const client = useApolloClient();
  const self = useRef<undefined | {}>(undefined);
  const [result, setResult] = useState<MyLazyQueryResult<T>>({
    loading: false,
    data: undefined,
  });
  const execQuery = useCallback(async () => {
    const current = {};
    self.current = current;
    try {
      setResult({
        loading: true,
        data: undefined,
      });
      const queryResult = await client.query({
        query,
        ...options,
      });
      if (self.current !== current) {
        // query canceled
        return;
      }
      setResult({
        loading: false,
        data: queryResult.data,
        error: queryResult.error,
      });
    } catch (error: any) {
      if (self.current !== current) {
        // query canceled
        return;
      }
      setResult({
        loading: false,
        data: undefined,
        error,
      });
    }
  }, [client, query, options]);
  return [execQuery, result];
}

madhugod avatar Jan 17 '22 18:01 madhugod

@madhugod I will look into that.

sztadii avatar Jan 19 '22 09:01 sztadii

You are right, the issue is there, and the unit test that I just wrote covers it. I will look at your solution and let's see what we can do to solve it.

sztadii avatar Jan 19 '22 19:01 sztadii

When I thought a little longer about the issue I have some thoughts. The current behavior is the same as useQuery and when variables will change then the query will re-fetch. To make useLazyQuery bulletproof we should make it simpler. @benjamn what do you think if we will remove options from useLazyQuery and we will be able to pass it only during execution?

const [execQuery, { loading, data, error }] = useLazyQuery(SEGMENT_QUERY)
...
<button onClick={() => execQuery({ variables: { value } })}>Submit</button>

So in this way, we will do not need to fix it ( if we consider it as a bug ) and developers will do not think that much about how to use useLazyQuery.

sztadii avatar Jan 19 '22 23:01 sztadii

@sztadii Personally I like this idea, it would make it work like a useMutation which I think is easier :)

madhugod avatar Jan 20 '22 08:01 madhugod

@madhugod so there are two of us that see it useful. We will need owners to agree on that 🤞

sztadii avatar Jan 20 '22 09:01 sztadii

I'd be quite glad with that change

eturino avatar Jan 20 '22 16:01 eturino

Let's make it three, it's crazy that this is not the default. Makes me wonder how else are people using this.

cesarvarela avatar Mar 11 '22 20:03 cesarvarela

I am also experiencing the same issue, the update of a state that changes the query variable automatically triggered the query to execute.

jhung0108 avatar Apr 07 '22 05:04 jhung0108

Hi, experiencing same issue with latest Apollo version 3.6.2. @sztadii good point, but isn't it workaround what you described? I mean, in general shouldn't useLazyQuery should work the way that it shouldn't be triggered on variables change?

I also noticed one issue with useQuery(Maybe this was intentional for new Apollo version), but still would like to point out and hear some thoughts. So here is example:

  1. Have a paginated list.
  2. Have a typePolicy custom merge function where I spread and return [...existingList, ...incomingList] (Unique by reference)
  3. Using const { data, loading, fetchMore } = useQuery(someQuery, { variables: { hasSeen: activeValue } }). 4 Initial activeValue is true and I do fetchMore and add 10 more items in a list. (Now we have 20 items in cache)
  4. If now I change activeValue to false (Which is in local state), useQuery will be triggered, which brings 10 new items, but in my custom merge function existingList contains old data as well, so I assume that useQuery doesn't do refetch on variable change.

So I wandering was this intentional change for new Apollo(3.6.2), as far as I remember on version 2.3 it was doing refetch and whole pagination was starting from initial state. Any thoughts? Thank you.

vanoMekantsishvili avatar May 09 '22 10:05 vanoMekantsishvili

@sztadii with useQuery, it seems logical to me that it refetch, but not for useLazyQuery as it should only redefine a new function (execQuery in the first example). Options for useLazyQuery aren't supposed to be "default options" while defining live options should be set with the return function

in author's case, in my opinion, it should be used like : `const [execQuery, { loading, data, error }] = useLazyQuery(SEGMENT_QUERY);

execQuery({ variables: { value } }, })`

jbcrestot avatar Jul 28 '22 16:07 jbcrestot

Hello, I'm facing some issues with useLazyQuery. Please take a look at the code snippet below:

import { useLazyQuery } from "@apollo/client";
import { useErrorContext } from "../contexts/handle-error.context";
import { UserByTokenQuery } from "../apis/queries/user.query";
import { useCallback } from "react";

export const useQueryHook = () => {
  const { handleError, clearError } = useErrorContext();
  const [getUserByToken] = useLazyQuery(UserByTokenQuery, {
    onError: (err) => {
      console.log(22, err);
    },
  });

  const handleGetUserByToken = useCallback(async () => {
    console.log(1111);
    const accessToken = window.sessionStorage.getItem("authenticated");
    if (!accessToken) return;
    const result = await getUserByToken({ variables: null });
    clearError();
    return result;
  }, [clearError, getUserByToken]);

  return {
    handleGetUserByToken,
  };
};

import { useMutation } from "@apollo/client";
import { SignInMutation, SignInVariables } from "../apis/mutations/signOut.mutation";
import { SignOutMutation } from "../apis/mutations/signIn.mutation";
import { useErrorContext } from "../contexts/handle-error.context";
import { useCallback } from "react";

export const useMutationHook = () => {
  const { handleError, clearError } = useErrorContext();

  const [signInMutation] = useMutation(SignInMutation, {
    onError: handleError,
  });
  const [signOutMutation] = useMutation(SignOutMutation, {
    onError: (err) => console.log(211222, err),
  });

  const handleSignIn = useCallback(
    async (username, password) => {
      const result = await signInMutation({
        variables: SignInVariables(username, password),
      });
      clearError();
      return result;
    },
    [clearError, signInMutation]
  );

  const handleSignOut = useCallback(async () => {
    console.log("signout ne");
    await signOutMutation();
    clearError();
  }, [clearError, signOutMutation]);

  return {
    handleSignIn,
    handleSignOut,
  };
};

import { useRouter } from "next/router";
import { useMutationHook } from "./use-mutation";
import { useQueryHook } from "./use-query";
import { useLazyQuery } from "@apollo/client";
import { UserByTokenQuery } from "../apis/queries/user.query";
import { useErrorContext } from "../contexts/handle-error.context";
import { useCallback } from "react";

export const useActionHook = () => {
  const router = useRouter();
  const { handleError } = useErrorContext();

  const { handleSignIn, handleSignOut } = useMutationHook();
  const { handleGetUserByToken } = useQueryHook();

  const loginAction = useCallback(
    async (username, password) => {
      const { data } = await handleSignIn(username, password);
      const { accessToken } = data.signIn;
      window.sessionStorage.setItem("authenticated", accessToken);
      console.log("loginAction ne");
      const { userByToken } = (await handleGetUserByToken())?.data;
      return userByToken;
    },
    [handleGetUserByToken, handleSignIn]
  );

  const logoutAction = useCallback(async () => {
    await handleSignOut();
    router.push("/auth/login");
  }, [handleSignOut, router]);

  return {
    loginAction,
    logoutAction,
  };
};

When logoutAction is called, I navigate the user to the login page. However, I don't understand why handleGetUserByToken is being called, and since I have deleted the token, it causes an error.

I don't understand why handleGetUserByToken is being triggered automatically each time my page is re-rendered.

versions: node: v18.15.0 "@apollo/client": "^3.7.17", "react": "18.2.0",

ankitruong avatar Aug 04 '23 20:08 ankitruong

@ankitruong , @jbcrestot , @sztadii

I was also facing a similar use and the issue in my case was that I had the dynamic query implemented with useLazyQuery().

As per the useLazyQuery() implementation, it subscribes to the subsequent changes to the cache. This means that the data always get recomputed and it has different values.

Maybe where the query should be fired when required in such situations. client.query() seems a good choice.

Here is the sample code

import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

// Initialize Apollo Client
const client = new ApolloClient({
  uri: 'https://your-graphql-endpoint.com',
  cache: new InMemoryCache()
});

// Define your query
const GET_DATA = gql`
  query GetData {
    data {
      id
      name
    }
  }
`;

// Use client.query() to fetch data
client.query({ query: GET_DATA })
  .then(response => console.log(response.data))
  .catch(error => console.error(error));

immayurpanchal avatar Jan 03 '24 04:01 immayurpanchal

Any updates on this?

I am still not sure whether you consider the automatic trigger a bug or expected behaviour, see https://github.com/apollographql/apollo-client/issues/7484#issuecomment-925314635. However, I could implemented the expected behaviour using useQuery in combination with skip: skip is only true when data should be fetched, i.e. it is false when the user changes the value in the input field and set to true when the user clicks on the search button. This requires another useState hook for skip.

@madhugod How did you solve it in the end?

tobiasschweizer avatar Jun 10 '24 06:06 tobiasschweizer

@tobiasschweizer I created a custom hook, see https://github.com/apollographql/apollo-client/issues/9317#issuecomment-1014792893

madhugod avatar Jun 10 '24 08:06 madhugod

So it's a permanent workaround now ;-)

tobiasschweizer avatar Jun 10 '24 08:06 tobiasschweizer