Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
August 22, 2021 05:52 pm GMT

Cancel fetch requests, and a way to abstract it

Working on another post/tutorial on fetch, I found myself needing to cancel individual fetch requests.

I investigated a bit, and learned about AbortController (supported in all browsers, except... can you guess who? yeah, IE).

Pretty neat stuff, let me show you how it's used, and I will explain it later on:

function fetchTodos(signal) {    return fetch('/todos', { signal });}function fetchUsers(signal) {    return fetch('/users', { signal });}const controller = new AbortController();fetchTodos(controller.signal);fetchUsers(controller.signal);controller.abort();

Okay, now let me break that down

First we define two functions that use fetch to retrieve some data, they also receive a signal argument (explained a bit further):

function fetchTodos(signal) {    return fetch('/todos', { signal });}function fetchUsers(signal) {    return fetch('/users', { signal });}

After that we create an instance of AbortController, this controller will allow us to get a signal to pass to fetch, and it also gives us the option to cancel the request:

const controller = new AbortController();

Then we just pass the signal property of the controller, to both fetch requests:

fetchTodos(controller.signal);fetchUsers(controller.signal);

What's this signal thing?

Well, basically it's a mechanism to communicate with a DOM request. Not directly though, a reference to the signal is passed to fetch, but, then abort using the controller, which internally interacts with the signal.

As you can see we are passing in the same signal to both requests, this means if we abort on the current controller, it will cancel all ongoing requests.

Finally at any point after running fetch, we can cancel the request (if it's not yet completed):

controller.abort();

Note: When abort() is called, the fetch() promise rejects with a DOMException named AbortError

BUT WAIT

What if we try to run fetchTodos again, after aborting?

// ... previous codecontroller.abort();fetchTodos(controller.signal);

If we pass the same signal it will instantly abort the request.
We would need to create a new controller and signal for the new request, becoming a bit tedious to add to each specific requests.

Lets see the solution I found, by returning a custom object, and generating a signal for each request:

The first thing we need is a class, that will wrap around the fetch promise and optionally the abort controller:

export class CustomRequest {    constructor(requestPromise, abortController) {        if(!(promise instanceof Promise)) {            throw TypeError('CustomRequest expects "promise" argument to be a Promise');        }        // Only check abort controller if passed in, otherwise ignore it        if(abortController && !(abortController instanceof AbortController)) {            throw TypeError('CustomRequest expects "abortController" argument to be an AbortController');        }        this.promise = requestPromise;        this.abortController = abortController;    }    abort() {        if (!this.abortController) return;        return this.abortController.abort();    }    then(fn) {        this.promise = this.promise.then(fn);        return this;    }    catch(fn) {        this.promise = this.promise.catch(fn);        return this;    }}

CustomRequest behaves almost exactly like a promise, but we add some extra functionality in the form of the abort method.

Next, create a wrapper around fetch, called abortableFetch, which will return a new CustomRequest instead of the regular fetch promise:

export function abortableFetch(uri, options) {    const abortController = new AbortController();    const abortSignal = abortController.signal;    const mergedOptions = {        signal: abortSignal,        method: HttpMethods.GET,        ...this.options,        ...options,    };    const promise = fetch(uri, mergedOptions);    return new CustomRequest(promise, abortController);}

Let us now change the original example, and apply the new fetch function:

function fetchTodos() {    return abortableFetch('/todos');}function fetchUsers() {    return abortableFetch('/users');}const todosReq = fetchTodos();const usersReq = fetchUsers();// We can now call abort on each individual requeststodosReq.abort();usersReq.abort();

Much better right?

We can even use is as a regular promise:

const todosReq = fetchTodos();todosReq.then(...).catch(...);

Another thing to notice, you can still override the signal in case you want to controll all requests with the same signal.

function fetchTodos() {    return abortableFetch('/todos', { signal: globalSignal });}

This signal will override the default one created in abortableFetch

Complete code

export class CustomRequest {    constructor(requestPromise, abortController) {        if(!(promise instanceof Promise)) {            throw TypeError('CustomRequest expects "promise" argument to be a Promise');        }        // Only check abort controller if passed in, otherwise ignore it        if(abortController && !(abortController instanceof AbortController)) {            throw TypeError('CustomRequest expects "abortController" argument to be an AbortController');        }        this.promise = requestPromise;        this.abortController = abortController;    }    abort() {        if (!this.abortController) return;        return this.abortController.abort();    }    then(fn) {        this.promise = this.promise.then(fn);        return this;    }    catch(fn) {        this.promise = this.promise.catch(fn);        return this;    }}export function abortableFetch(uri, options) {    const abortController = new AbortController();    const abortSignal = abortController.signal;    const mergedOptions = {        signal: abortSignal,        method: HttpMethods.GET,        ...this.options,        ...options,    };    const promise = fetch(uri, mergedOptions);    return new CustomRequest(promise, abortController);}function fetchTodos() {    return abortableFetch('/todos');}function fetchUsers() {    return abortableFetch('/users');}const todosReq = fetchTodos();const usersReq = fetchUsers();// We can now call abort on each individual requeststodosReq.abort();usersReq.abort();

Summary

Well, for me, this is another weird API, if I'm honest. It does the job, but could have been done better. That aside, we can do some stuff around it and improve our experience a bit.

And to recap, in this post we've:

  • seen how to cancel requests in the most simple way,
  • detected some weird or tedious things,
  • and finally built something on top of it to help us ease the process!

Links

Another quick post, I was in a writing mode this weekend so... I hope you liked it, and found it usefull!

If you did, consider supporting me by reacting to the post, following me here or over on GitHub, or commenting!

Original Link: https://dev.to/nombrekeff/cancel-fetch-requests-and-a-way-to-abstract-it-3gib

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To