Asynchronous adventures: Aborting queries and mutations in react-apollo
Tags: apollo, react, graphql, abortquery
Published: 2019-07-09 10:25:10

---
title: Asynchronous adventures: Aborting queries and mutations in react-apollo
published: true
canonical_url: https://evilmartians.com/chronicles/aborting-queries-and-mutations-in-react-apollo
description: How to cancel a mutation in Apollo with the help of AbortController
tags: apollo, react, graphql, abort query
---

_TLDR: If you just want to cancel a query or mutation in *react-apollo*,  you can skip an intro and jump directly to a [recipe](#howto)._

## Why do I ever need to cancel a request in React Apollo?

Let's take an interface that sends a bunch of consecutive requests where the only last one is the one that matters. It can be an autosuggest input, or a form with an automatic save on every change. To work correctly, an application have to use a response of the last request and ignore previous results (even though the previous request may yield the result **after** the last one).

In a normal situation, react-apollo will do it for you automatically. For instance, imagine a field for a postal code on the e-commerce web-site. Its contents are saved and automatically checked to determine whether shipping is possible to a given destination:

```jsx
import * as React from "react";
import { Mutation } from "react-apollo";
import gql from 'graphql-tag';

const saveZipCode = gql`
  mutation SaveZipCode($input: String) {
    save(input: $input) {
      hasShipping
    }
  }
`;

function ZipCodeField(props) {
  return (
    <Mutation mutation={saveZipCode}>
      {(save, { data }) => (
        <div>
          <input
            onChange={({ target: { value } }) =>
              save({ variables: { input: value } })
            }
          />
          {data.hasShipping && <div>Shipping is available!</div>}          
        </div>
      )}
    </Mutation>
  );
}

```

In the example above, every change of the input field will call the save mutation and receive the `hasShipping` flag that tells if shipping is available. What we want is to ignore results of all previous mutations that happened while a user was typing in the code. 

Luckily, Apollo does it for us: if `<Mutation>` component has a previous mutation in progress—it will be automatically canceled as soon as the new one takes place. 

## Debounce mutation

Performing a mutation on every change is usually a bad idea because it puts extra load both on a network and on your back-end. It is better to debounce user's input and fire a request only _after_ the user has stopped typing. 


```jsx
// There are plenty of 'debounce' implementations out there. We can use any.
import debounce from "lodash-es/debounce";


// ....

function ZipCodeField(props) {
  const debouncedSave = React.useRef(
    debounce((save, input) => save({ variables: { input } }), 500 )
  );


  return (
    <Mutation mutation={saveZipCode}>
      {(save, { data }) => (
        <div>
          <input
            onChange={({ target: { value } }) => debouncedSave.current(save, value)}
          />
        </div>
        {data.hasShipping && <div>Shipping is available!</div>}          
      )}
    </Mutation>
  );
}
```

This code will postpone saving mutation for 500ms after the last change. Any intermediate changes will not fire a mutation at all. 

However, this solution has a flaw. If an interval between two change events is slightly more than 500ms—both mutations will be fired, but Apollo won't be able to cancel the first one for at least 500ms of the second debounce interval, because actual mutation has not been called yet. Here is the possible timeline of events:

*000ms*: 1st `onChange`—debounce mutation for 500ms.

*500ms*: fire 1st mutation's request.

*501ms*: 2nd `onChange`—debounce second mutation for 500ms (Apollo doesn’t know about a second request and therefore can not cancel the first one)

*600ms*: 1st mutation's response. Now interface is updated with the result of the first mutation, but the input field has more text to send for the second mutation. Different parts of our interface are out of sync now.

*1000ms*: fire 2nd mutation's request (it is too late to cancel 1st request)

*Somewhere in the future*: 2nd mutation response. Now the system gains consistency again

There is a gap between the first and the second mutations' responses, during which our interface is out of sync. Input field has a postal code that was sent in the second mutation but interface shows a result of the previous postal code's check. These may lead to the unpleasant UX, or even some serious race condition bugs.

One of the best (and easiest) ways of fixing it is to manually cancel the first mutation immediately after the second `onChange` event. Luckily, there is a way to do it in Apollo, although it is not well documented.

<a name="howto"></a>
## Use AbortController API for Apollo requests cancellation 

_WARNING! According to [this issue](https://github.com/apollographql/apollo-client/issues/4150) using abort controllers doesn't work with GraphQL queries. It works for mutations but may have unexpected side effects in some configurations. There is a [PR](https://github.com/apollographql/apollo-link/pull/880) fixing this issue that is not merged yet._

In its standard configuration, Apollo uses the browser's `fetch` API for actual network requests and it is possible to pass arbitrary options to it. So we can use [Abort Signals](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) to abort any mutation:

```js
// Create abort controller
const controller = new window.AbortController();

// Fire mutation
save({ options: { context: { fetchOptions: { signal: controller.signal } } } });

// ...

// Abort mutation anytime later
controller.abort()
```

*[AbortController API](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) is still in an experimental stage, so don't forget to polyfill it if you care about old browsers.*

## Enhanced example with debouncing and aborting previous requests

With the help of abort signals we can cancel an old request on every `onChange` to make sure we will always show results only for the last one:

```jsx
function ZipCodeField(props) {
  const abortController = React.useRef();
  const debouncedSave = React.useRef(
    debounce((save, input) => {
      const controller = new window.AbortController();
      abortController.current = controller;

      save({
        variables: { input },
        options: {
          context: { fetchOptions: { signal: controller.signal } }
        }
      });
    }, 500)
  );

  const abortLatest = () =>
    abortController.current && abortController.current.abort();

  return (
    <Mutation mutation={saveZipCode}>
      {(save, { data }) => (
        <div>
          <input
            onChange={({ target: { value } }) => {
              abortLatest();
              debouncedSave.current(save, value);
            }}
          />
          {data.hasShipping && <div>Shipping is available!</div>}          
        </div>
      )}
    </Mutation>
  );
}
```

Here we create an `AbortController` for every mutation and save it to `abortController` ref. Now we can manually cancel an ongoing mutation when postal code is changed by calling `abortController.current.abort()`

For simple situations like this, custom Apollo link might be the better option. But if you need a fine grained control over your requests Abort Signals is a good way to achieve it. 

Thank you for reading!

---

Read more dev articles on https://evilmartians.com/chronicles!