apollo-client: UseLazyQuery does not trigger onCompleted if result remains the same

Intended outcome:

I want to query manually using useLazyQuery hook and get the result in the onCompleted callback function. I also use a no-cache fetch policy.

Actual outcome:

The onCompleted callback function is only triggered the first time, if the result of the query has changed. After the first call, I can see in the Network panel of my browser that the query is being executed everytime I click on the button but no further event is being fired. It would be logical that the onCompleted callback function triggers no matter what the fetch policy is.

How to reproduce the issue:

function TestComponent() {
  const [execQuery] = useLazyQuery(MY_QUERY, {
    fetchPolicy: 'no-cache',
    onCompleted: result => {
      console.log('execQuery has completed'); // only fires the first time
    },
  });

  return (
    <>
      <button onClick={() => execQuery()}>click me</button>
    </>
  );
}

Versions

apollo-client : 3.5.7 react : 17.0.2

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 38
  • Comments: 24 (1 by maintainers)

Commits related to this issue

Most upvoted comments

fetchPolicy: 'cache-and-network' works for me.

Apollo client library which has over 2m downloads per week doesn’t have any docs regarding how onCompleted behaves. It works totally randomly depending on fetch-policy and apollo/client version. This subject is not new…waiting for over 2 years and still no solution/no docs.

We changed a number of things about how useQuery and useLazyQuery are implemented internally in Apollo Client v3.6, so I would recommend attempting to update with npm i @apollo/client@latest when you have a chance!

For example, useLazyQuery is now implemented (more fully) in terms of useQuery, so it should now (with any luck) inherit the same onCompleted and onError calling behavior as useQuery. Even if that behavior is undesirable, this means we can adjust/fix both hooks simultaneously, going forward.

@jmtimko5 I don’t think this is a bug, it is just the default behavior. It can be a bit annoying that the default behaviour isn’t network-only, but it is not a bug 🤷

By default, the useQuery hook checks the Apollo Client cache to see if all the data you requested is already available locally. If all data is available locally, useQuery returns that data and doesn’t query your GraphQL server. This cache-first policy is Apollo Client’s default fetch policy.

Another workaround may be to not rely on onCompleted and use the promise’s then() instead:

  const [getUsers, { loading, data }] = useLazyQuery(USERS_QUERY);
  // ...
  getUsers().then((response) => {
    setUserList(response.data.users.data);
  });

(Also, not sure when lazyQuery started returning a promise. It doesn’t appear to be in v3.3, but is in v3.7.)

This is pretty brutal bug honestly. It’s completely unexpected behavior that has led to several bugs leaking into production in our product. Can someone on the Apollo side investigate and confirm it is not present on the latest version. I do see a pattern on a lot of these Apollo bugs where there is no attention for months, a new version gets released, then maintainers say please upgrade to the latest version. There is no investigation whether the bug is still present in the new version. Often people confirm the latest version still has the bug, and then the bug remains open or worse it is preemptively closed.

Hi, you need to use notifyOnNetworkStatusChange: true. Can confirm that notifyOnNetworkStatusChange solves the problem.

Another workaround may be to not rely on onCompleted and use the promise’s then() instead:

  const [getUsers, { loading, data }] = useLazyQuery(USERS_QUERY);
  // ...
  getUsers().then((response) => {
    setUserList(response.data.users.data);
  });

(Also, not sure when lazyQuery started returning a promise. It doesn’t appear to be in v3.3, but is in v3.7.)

This is what I ended up doing to resolve the issue. When in doubt, rely on the underlying JS API, if there’s not some huge tradeoff in readability. I don’t know how onCompleted works. I DO know how Promises work.

Ran into this again in a customer demo, just a bummer. 1 year to the day where this caused a production fire previously.

Another workaround may be to not rely on onCompleted and use the promise’s then() instead:

  const [getUsers, { loading, data }] = useLazyQuery(USERS_QUERY);
  // ...
  getUsers().then((response) => {
    setUserList(response.data.users.data);
  });

(Also, not sure when lazyQuery started returning a promise. It doesn’t appear to be in v3.3, but is in v3.7.)

The onComplete callback didn’t always fire when triggering calls quickly after one another, even though all the calls were executed in the network tab. The promise approach also works for me.

@benjamn It becomes a question of whether it’s a good design choice to have onComplete only fire the first time. Keen on thoughts. Thanks for the explainer on the recent changes.