Skip to main content

Aborting Fetch

AbortController provides a new way of cancelling fetches that are no longer considered relevant. This can be hooked into fetch via the second RequestInit parameter.

Resource

Easy integration is provided with the RestEndpoint via the signal member:

const abort = new AbortController();
const AbortableArticle = CoolerArticleResource.get.extend({
signal: abort.signal,
});
// ...somewhere later trigger cancellation
abort.abort();

Endpoint

Additionally similar functionality can easily be added to any endpoint using custom members.

type Params = { id: string };

const UserDetail = new Endpoint(
function ({ id }: Params) {
const init: RequestInit = {};
if (this.signal) {
init.signal = this.signal;
}
return fetch(this.url({ id }), init).then(res => res.json()) as Promise<
typeof payload
>;
},
{
url({ id }: Params) { return `/users/${id}` },
signal: undefined as AbortSignal | undefined,
},
);
const abort = new AbortController();
const AbortableUserDetail = UserDetail.extend({
signal: abort.signal,
});
// ...somewhere later trigger cancellation
abort.abort();

Cancelling on params change

Sometimes a user has the opportunity to fill out a field that is used to affect the results of a network call. If this is a text input, they could potentially type quite quickly, thus creating a lot of network requests.

Using @data-client/hooks package with useCancelling() will automatically cancel in-flight requests if the parameters change before the request is resolved.

export class Todo extends Entity {
  id = 0;
  userId = 0;
  title = '';
  completed = false;
  pk() {
    return `${this.id}`;
  }
  static key = 'Todo';
}
export const TodoResource = createResource({
  urlPrefix: 'https://jsonplaceholder.typicode.com',
  path: '/todos/:id',
  schema: Todo,
});
import { useSuspense } from '@data-client/react';
import { useCancelling } from '@data-client/hooks';
import { TodoResource } from './api/Todo';

export default function TodoDetail({ id }: { id: number }) {
  const todo = useSuspense(useCancelling(TodoResource.get, { id }), {
    id,
  });
  return <div>{todo.title}</div>;
}
import React from 'react';
import { AsyncBoundary } from '@data-client/react';
import TodoDetail from './TodoDetail';

function AbortDemo() {
  const [id, setId] = React.useState(1);
  return (
    <div>
      <AsyncBoundary fallback="...">
        <TodoDetail id={id} />
      </AsyncBoundary>
      <div>
        <button onClick={() => setId(id => Math.max(id - 1, 1))}>
          «
        </button>
        {id} &nbsp;
        <button onClick={() => setId(id => id + 1)}>»</button>
      </div>
    </div>
  );
}
render(<AbortDemo />);
🔴 Live Preview
Store

Try clicking the » very quickly. If you increment before it resolves the request will be cancelled and you should not see results in the store.

Warning

Be careful when using this with many disjoint components fetching the same arguments (Endpoint/params pair) to useSuspense(). This solution aborts fetches per-component, which means you might end up canceling a fetch that another component still cares about.