Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useLazyQuery automatically triggers after being triggered once #9317

Open
madhugod opened this issue Jan 17, 2022 · 16 comments
Open

useLazyQuery automatically triggers after being triggered once #9317

madhugod opened this issue Jan 17, 2022 · 16 comments

Comments

@madhugod
Copy link

madhugod commented Jan 17, 2022

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
Copy link
Author

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];
}

@sztadii
Copy link
Contributor

sztadii commented Jan 19, 2022

@madhugod I will look into that.

@sztadii
Copy link
Contributor

sztadii commented Jan 19, 2022

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
Copy link
Contributor

sztadii commented Jan 19, 2022

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.

@madhugod
Copy link
Author

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

@sztadii
Copy link
Contributor

sztadii commented Jan 20, 2022

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

@eturino
Copy link

eturino commented Jan 20, 2022

I'd be quite glad with that change

@cesarvarela
Copy link

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

@jhung0108
Copy link

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

@vanoMekantsishvili
Copy link

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.

@jbcrestot
Copy link

@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 } },
})`

@ankitruong
Copy link

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",

@immayurpanchal
Copy link

immayurpanchal commented Jan 3, 2024

@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));

@tobiasschweizer
Copy link

Any updates on this?

I am still not sure whether you consider the automatic trigger a bug or expected behaviour, see #7484 (comment).
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?

@madhugod
Copy link
Author

@tobiasschweizer I created a custom hook, see #9317 (comment)

@tobiasschweizer
Copy link

So it's a permanent workaround now ;-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests