apollo icon indicating copy to clipboard operation
apollo copied to clipboard

"onResult" called only once when same query used in two components

Open karladler opened this issue 3 years ago • 14 comments

Describe the bug using onResult hook, when the exact same query is also used within another component, will only be executed within one component. I'm not 100% sure if this is a bug but if not, it should be probably mentioned in the docs.

To Reproduce

  • create two components using the same query
  • add onResult() hook two both of them
  • render both components at the same time
  • onResult() will be triggered only once, not in both components

Expected behavior

  • onResult shall be triggered in all components where used

Versions vue: 2.6.12 @vue/apollo-composable: 4.0.0-alpha.12 @vue/apollo-util: 4.0.0-alpha.6 @vue/composition-api": 1.0.0-beta.25,

karladler avatar Feb 24 '21 21:02 karladler

That's misleading indeed. Also onResult() is executed only on first component mount with default fetch policy. If component was previously mounted and query response was fully cached by apollo, onResult() will not be executed on next component mount.

Al-Rozhkov avatar Feb 25 '21 08:02 Al-Rozhkov

Just experienced this as well. The fetch policy was set to cache-first. In first component (on empty cache), onResult was triggered. In second component (populated cache), onResult was never triggered. Workaround is that in the second case, the result from useQuery already contains the data you need, so we did something like this:

const { result, onResult } = useQuery(...);

const handleResult = (data) => {
  // Do side effect based on result
};

// Handle if result is already loaded from cache
if (result.value) handleResult(result.value);

// Handle if result is to be fetched from server
onResult(({ data }) => handleResult(data));

Generally, in team settings this is still problematic as it requires the team members to know the nuance of when to use the data from result, and when to use the onResult. If you're not familiar with this behaviour, the debugging isn't easy neither, bc if you test the components in isolation, and the query is executed only once, everything works.

A suggested solution to make it more intuitive would be to trigger onResult immediately if the result comes from the cache. (e.g. similar behaviour to watch with immediate: true).

JuroOravec avatar Apr 01 '21 07:04 JuroOravec

@JuroOravec, if you use a watch on result it pretty much solves the issue you're having with needing team members to know the nuance between onResult and result.

const { result, onResult } = useQuery(...);

const handleResult = (data) => {
  // Do side effect based on result
};

watch(result, () => {
  // Needed for the first time the component is mounted as the watch is invoked
  // immediately and result will not have any data.
  if (!result.value) {
    return;
  }
  handleResult(result.value);
}, {
  // Needed for when the component is mounted after the first mount, in this case
  // result will already contain the data and the watch will otherwise not be triggered.
  immediate: true
});

onResult(({ data }) => {
  // Perform actions only needed when data is returned from server.
});

I think in most of cases you will not need the onResult and just using result will be good enough. I was having the same problem as described in this issue and the setup above is what I am using now in my project. It works as intended and it doesn't have the issue of needing both onResult and result.

tbusser-io avatar Apr 21 '21 11:04 tbusser-io

Using a watch with immediate: true is what worked for me. immediate guarantees the handler will run on setup as well as when result changes.

const { result } = useQuery(...)

let channel = reactive({ name: '' })

watch(
  () => result,
  (res) => channel = { ...res.value?.channel },
  { immediate: true },
)

Soviut avatar May 25 '21 03:05 Soviut

I have the same issue, on the server watch with immediate: true isn't call twice. The main problem is in the hydration of the data, because need to perform the side effect. I'm using Nuxt.JS 2 and I'm trying to upgrade to Apollo 3. In my case, i was to able achieve the desired behavior like this:

const { result, onResult } = useQuery(...);

const handleResult = (data) => {
    if (!result.value) {
        return;
    }
    
    // Do side effect based on result
};

if (process.server) {
    onResult(handleResult);
} else {
    watch(result, handleResult, { immediate: true });
}

negezor avatar Aug 17 '21 13:08 negezor

Can't just onResult be called with cached result also? It'd solve all problems.

xxSkyy avatar Sep 02 '21 14:09 xxSkyy

Can't just onResult be called with cached result also? It'd solve all problems.

100% agree 👍

MCYouks avatar Feb 04 '22 19:02 MCYouks

{fetchPolicy: "network-only"} (or "no-cache") (documentation) seems to work as a band-aid solution.

0xBADCA7 avatar Feb 05 '22 00:02 0xBADCA7

{fetchPolicy: "network-only"} (or "no-cache") (documentation) seems to work as a band-aid solution.

No doubt, but caching the response offers great UX benefits.

Calling onResult systematically (including with cached results) would solve the problem.

MCYouks avatar Feb 05 '22 14:02 MCYouks

I find a solution to force trigger onResult method

you can create a ApolloLink to handle response and in the link, set a random property to response ,make the every query's response is different

like this forceOnResultFlag

const timerLink = new ApolloLink((operation, forward) => {
  // Called before operation is sent to server
  operation.setContext({ start: new Date() })
  console.log(`[GraphQL >>>][${operation.operationName}]`, operation.variables)

  return forward(operation).map((res) => {
    // Called after server responds
    res.data!.forceOnResultFlag = new Date().getTime()
    const time = new Date().getTime() - operation.getContext().start.getTime()
    console.log(`[GraphQL <<<][${operation.operationName}][${time}ms]`, res.data)
    return res
  })
})

remember add the link to your apolloClient

const apolloClient = new ApolloClient({
  link: from([timerLink, errorLink, authLink, httpLink]),
  cache: new InMemoryCache(),
  defaultOptions: defaultOptions,
})

eric-kuan avatar Feb 15 '22 05:02 eric-kuan

One thing that worked for me, was to get the variables of the query back and reset them. I could even use cache.

This is the query declaration (note the useLazyQuery)

const {
  onResult,
  loading,
  variables: queryVariables,
  load: loadQuery,
} = useLazyQuery(myQuery, null);

Then at some point I have a button that set variables and triggers the load:

queryVariables.value = {
  a: 1,
  b: 2,
};

loadQuery();

and on the onResult I clear the variables after the desired effect:

onResult(result => {
  // ... stuff happening

  queryVariables.value = {};
});

mauricioaraldi avatar Feb 25 '22 09:02 mauricioaraldi

Thanks, @tbusser-io for the immediate: true solution, it solved the issue for me. Using result directly might fix this problem but I was relying on onResult for some side effects on the result. I found creating a wrapper around useQuery that calls onResult with the cached results would be the best workaround. Something like:

/**
 * Workaround for `result` and `onResult` not triggering for cached data.
 * This behaviour causes the side effects in onResult or watchers to fail and thereby shows empty data
 * @param {Document} query GraphQL Tag
 * @param {Object} variables Query variables. Can be Object or function
 * @param {Object} options Options for useQuery
 * @returns {Object}
 */
function useReactiveQuery (query, variables, options) {
  const q = paramToRef(query);
  const v = paramToRef(variables);
  const o = paramToRef(options);
  const result = ref();
  const { refetch, result: queryResult, onResult: onQueryResult, loading, error } = useQuery(q, v, o);
  watch(queryResult, res => {
    result.value = res;
  }, { immediate: true });
  onQueryResult(response => {
    result.value = response.data;
  })
  return { result, onResult: onQueryResult, loading, error, refetch };
};

vishnu-nt avatar Mar 09 '22 10:03 vishnu-nt

I've made helper package that helps with this and some other problem I've faced in all my projects, feel free to use it till this issue wont be resolved.

xxSkyy avatar Apr 23 '22 22:04 xxSkyy

Why does onResult even exist if it's not called even after a refetch and we should just watch the result anyway?

kevlarr avatar Jun 07 '22 14:06 kevlarr

Bump, this drove me insane because I couldn't figure out why onResult doesn't get called after remounting my component. The documentation is misleading: "onResult: This is called whenever a new result is available."

Can't just onResult be called with cached result also? It'd solve all problems.

This is the way to go and should be addressed immediately. Or at least for the time being the documentation should be updated with a disclaimer that onResult is not called when the result is coming from the cache.

felixxxxxs avatar Mar 26 '23 17:03 felixxxxxs

Could not manage the same problem. I tried to depend on refetchQueries: 'active' to refresh state of component. But onResult didn't work if response from server was the same as last time. Watching result didn't help also

outluch avatar Oct 30 '23 22:10 outluch