Search code examples
javascriptreactjsreact-querytanstackreact-query

Canceling a react-query mutation


I have a mutation that comprises of a sequence of long-running actions:

useMutation({
  mutationFn: async () => {
    await firstTask()
    await secondTask()
    await thirdTask()
  },
  ...
})

I know I can't cancel the individual tasks once they are in-flight, but I'd like to prevent the execution of any of the tasks that haven't been executed yet once a 'Cancel' button elsewhere in the UI has been clicked.

One option is to pass a ref to mutate and check it in between tasks. If it's false continue. If it's true exit. Then set the ref to false from the cancel button.


Solution

  • The only thing that can stop a function from running is that function itself. So if you want the mutationFn to stop partway through, add code something like this:

    const isCancelled = useRef(false);
    useMutation({
      mutationFn: async () => {
        isCancelled.current = false;
        await firstTask()
        // If you prefer, you could throw an error instead of returning
        if (isCancelled.current) return;
        await secondTask()
        if (isCancelled.current) return;
        await thirdTask()
      },
      ...
    })
    ...
    <button onClick={() => { isCancelled.current = true }}/>
    

    I know I can't cancel the individual tasks once they are in-flight

    That's mostly true, but i want to point out that if the tasks are making fetch requests, fetch supports abort signals. So if that's something you want to do, then instead of storing a boolean isCancelled you can store an AbortController

    const controller = useRef(null)
    useMutation({
      mutationFn: async () => {
        controller.current = new AbortController();
        const signal = controller.current.signal
        await firstTask(signal); // change this code to use the signal
        if (signal.aborted) return;
        await secondTask(signal);
        if (signal.aborted) return;
        await thirdTask(signal);
      },
      ...
    })
    ...
    <button onClick={() => { controller.current?.abort() }}/>