Skip to content

useMutation's onSuccess promise never resolves due to setQueryData and refetchQuery edge case #639

@franleplant

Description

@franleplant

Describe the bug

I have created a very contrived example reproduction code that you can find here:
https://github.com/franleplant/react-query-unresolved-promises/blob/master/src/App.tsx

I say contrived because it doesn't represent real use case but I first found this error
in a closed source company app where, as you probably know, things started to get messy
and there were some interactions between mutation's onSuccess and refetchQueries.

To be a bit more concrete, when we save EntityA we refetch EntityA (because of the way our particular backend works) but since we weren't using exact: true it was causing it to refetch EntityA1 via a query that wasn't initialized, so by a semi complex interactions of the query of EntityA we also did setQueryData of EntityA1 which caused the problem I explain below.

(Sorry, it ended up not being that much more concrete)

I found this bug in react-query 1.x but I have noticed the same code in 2.x

Expected behavior

A mutation promise always resolves to something, either a value or an error.

Desktop (please complete the following information):

  • OS: macos
  • Browser all

Additional context

I have isolated the bug to an edge case interaction between queryCache.setQueryData and queryCache.refetchQuery

(extracted from the repro repo)

export function useSomeMutation() {
  return useMutation<void, undefined>(
    async () => {
      await delay();
    },
    {
      onSuccess: async () => {
        console.log("onSuccess start");
        queryCache.setQueryData("notInstantiated", "hello"); //BANG
        queryCache.setQueryData("notInstantiated2", "hello"); // BANG
        const a = queryCache.refetchQueries("notInstantiated", { force: true });
        const b = queryCache.refetchQueries("notInstantiated2", {
          force: true,
        });
        console.log("promises are never resolved", a, b);
        await Promise.all([a, b]);
        // This never reaches
        console.log("onSuccess end");
      },
    }
  );
}

Whenever you try to setQueryData on a query that hasn't been instantiated react-query gives the queryFn a value of new Promise(noop) which is a promise that never resolves.
So when you later do refetchQuery of that same query you get a promise that never returns and since useMutation waits for the onSuccess async function to resolve then it never resolves.

We are virtually saying this

onSuccess: () => new Promise(noop)

This same default value of new Promise(noop) is used in 2.x branch.

for lib consumers: A workaround is to check that the query that you are trying to setData on has actually an instance

// check that there are current instances of `myQueryKey`
// i.e. some component has used it
if (queryCache.getQuery(myQueryKey)) {
  queryCache.setQueryData(myQueryKey, someData)
}

But I wander why we are giving a default value of a promise that never resolves. Can you we change it to Promise.resolve() ? It seems that the functionality will be maintained but in these sort of edge cases we wont get a promise that never resolves.

Let me know what you think @tannerlinsley and I can definitively help with this in both 1.x and 2.x branches.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions