query: Mutations can not be started from an effect in Reacts StrictMode

Describe the bug

When firing a mutation from a useEffect on component creation the mutation get’s stuck in loading state, despite the underlying request finishing. This only seems to happen in Reacts StrictMode.

If you naively do this you won’t notice the bug, because this effect will fire twice in StrictMode and therefore “unstuck” the mutation when the request is sent the second time.

useEffect(() => {
  if (isIdle) mutate()
}, [isIdle, mutate])

That’s why the codesandbox example has a ref that keeps track on if the mutation has fired, to ensure it only runs once.

The reason we’re doing this in the first place is because we need to fire several mutations and keep track of the loading states individually, so we use components to create several mutation instances and fire the mutations on effect instead of in the original event handler.

Your minimal, reproducible example

https://codesandbox.io/s/mutation-with-suspense-onsuccess-forked-lkmo6v?file=/src/Test.tsx

Steps to reproduce

  1. Go to attached codesandbox
  2. Observe that the two loading states differ (reload if you have to)

Expected behavior

I expect react-query’s loading state to have the same value as the manually coded loading state

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

  • OS: MacOS
  • Browser: Chrome

Tanstack Query adapter

react-query

TanStack Query version

v4.28

TypeScript version

v5.0.4

Additional context

No response

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 1
  • Comments: 17

Most upvoted comments

My use case is this:

  • User logs in from an OAuth provider. The provider redirects user back to a callback page. Maybe it’s using PKCE or something so it’s fine to do this client side.
  • Now we are implementing an OAuth callback page, so, as soon as a callback page is mounted, the page component should attempt to use the code to sign the user in.
export default function () {
  const mutation = useMutation({
    mutationFn: async () => {
      const params = new URLSearchParams(window.location.search)
      const code = params.get('code')
      const state = params.get('state')
      // ...
    },
  })
  useEffect(() => {
    mutation.mutate()
  }, [])
  // ...
}

The problem is that, in the above, mutationFn is called twice.

We could have used useQuery here, but the act of signing in is a mutation, so it doesn’t make much sense to use useQuery here. We also don’t want to deal with automatic retries and automatic refetching, i.e. we want the “mutation” behavior.

Using useRef to only call mutation.mutate() does not work, as per this comment: https://github.com/TanStack/query/issues/5341#issuecomment-1527727996

mutations shouldn’t be side-effects of components rendering or mounting.

We oftentimes have to implement pages/routes whose sole purpose is to perform some action after loading, and then redirect user to another page. OAuth callbacks for public clients is one example. Payment processing is another example.

That’s what StrictMode is trying to tell us. Users can go to your page, navigate back and go there again. Then, you’ll also have two mutations.

…and this is the solution:

image

Currently we work around it like this:

// Move the mutation code outside the component,
// and use some extra logic to prevent it from running simultaneously.
let activeSignInPromise: Promise<void> | null = null
function signIn() {
  if (!activeSignInPromise) {
    activeSignInPromise = (async () => {
      try {
        const params = new URLSearchParams(window.location.search)
        const code = params.get('code')
        const state = params.get('state')
        // ...
      } finally {
        activeSignInPromise = null
      }
    })()
  }
  return activeSignInPromise
}

export default function () {
  const mutation = useMutation({ mutationFn: signIn })
  useEffect(() => {
    mutation.mutate()
  }, [])
  // ...
}

Edit: Found a simpler workaround:

  useEffect(() => {
    // Wait for a while to avoid double-mutation on dev + StrictMode.
    // See: https://github.com/TanStack/query/issues/5341
    const timeout = setTimeout(() => mutation.mutate(), 100)
    return () => clearTimeout(timeout)
  }, [])

…but I couldn’t expect junior devs to always implement this pattern correctly, therefore I wish there is an easier way to do it with React Query.

don’t put them in a useEffect (they should likely run in event handlers anyways)

I would love to put it in event handlers too! But is there any event suitable for implementing such callback page? I would like to avoid something like this if possible:

<img
  src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='
  alt=''
  onLoad={() => mutation.mutate()}
/>

Any suggestion is appreciated.

@TkDodo I ran into the exact same issue: after I fire the mutation once, the status is stuck in pending. The underlying mutation fires onSuccess, so why does it not transition to the success state? This is a bug.

In our case we need to POST once for a given combination of data. The useEffect dependencies make sure we only fire once for that combination. We use the useRef workaround to avoid firing twice in react-strict mode. Any workaround makes the code even more unreadable.

  const currentRun = useRef('');

  // after the effect runs, status is stuck in pending
  const { mutate, isSuccess, status } = useMyCustomMutation();

  useEffect(() => {
    if (eventId && 
      user?.id && 
      currentRun.current !== `${eventId}-${user.id}`) {
      currentRun.current = `${eventId}-${user.id}`;
      mutate({
        eventId,
        userId: user.id,
      })
    }
  }, [mutate, eventId, user?.id]);

As to your workarounds:

ignore the double fire, as it only happens in dev and not in prod

Impossible if the underlying API call is not idempotent. Like a POST that is working on the first call, and fails on the second call.

not use strict mode

Why should I remove strict mode? It is making the code more robust in a lot of ways.

not fire a mutation in useEffect

Please explain how I would create the same behavior without useEffect?

fire a request in useEffect without react-query’s mutations and re-add your ref workaround

Great, so now I have to re-create react-query’s very helpful state management, debugging and tooling? Not an option.

Also note that in the official react PR that added StrictMode, the useRef workaround was mentioned for “strictly once” behavior: https://github.com/reactwg/react-18/discussions/18

Please, reopen this issue and fix this. Not every mutation is called from an event handler.

sorry, I’m not going to invest time into issues caused by workarounds (refs) to make StrictMode not do something in dev mode that it’s intended to do. You can:

  • ignore the double fire, as it only happens in dev and not in prod
  • not use strict mode
  • not fire a mutation in useEffect
  • fire a request in useEffect without react-query’s mutations and re-add your ref workaround

What is the business requirement ?

Anyways, there is no change in production. Let it fire twice in dev mode, it’s fine in prod 🤷

the problem is that your hasFired ref workaround seems to screw things up. working sandbox: https://codesandbox.io/s/mutation-with-suspense-onsuccess-forked-2gekg2?file=/src/Test.tsx

Yeah but your modified example only works because it’s firing twice. The first call gets stuck on loading, and then the second one unstucks it. Do you mean that you do not consider this a bug?

If you don’t want mutations to fire twice, either don’t put them in a useEffect (they should likely run in event handlers anyways), or turn off strict mode

Yeah I would definitely prefer to run it in the event handler, but for our use case there is no way to do this with react-query AFAICT.

We want to fire an unknown amount of mutations with the same useMutation hook from our event handler, and then track each loading state individually. In some other issue thread I found your reply saying a solution was to run the mutation in the leaf component and trigger it on effect, so I figured that would be a reasonable way forward.

Is there any other pattern we could use here?