How to Abort Requests in Pinia Stores

Brenton Klik
3 min readDec 7, 2022

--

We often write Pinia stores for communicating with an API and saving the results of our requests. Sometimes, the user will make several quick requests to an API. If we fail to abort previous requests, the responses may become out of sync and the data presented to the user will be inaccurate.

Let’s say we have the following store…

import { defineStore } from 'pinia';
import { api } from 'src/boot/axios';
import { Person } from 'src/models/';

export const usePeopleStore = defineStore('people', {
state: () => ({
people: [] as Array<Person>,
error: '' as string,
loading: false as boolean,
}),
getters: {},
actions: {
async getPeople(options = '') {
this.loading = true;

await api
.get('/api/people/' + options)
.then((response) => {
this.people = response.data;
this.error = '';
this.loading = false;
})
.catch((error) => {
this.error = error;
console.error(error);
this.loading = false;
});
}
}
});

This is a simple ‘People’ store that contains an array of ‘Person’ objects. It has a ‘getPeople’ action that sends a request to an API to retrieve data. It has a ‘loading’ flag that can be watched so we know when a request is in progress. The action will also accept an ‘options’ string which gets passed to the API request. This can be used for searching and filtering the request.

Now imagine that we’re displaying a paginated list of people from this data. The user wants to jump a few pages in so they mash the ‘Next Page’ button a few times in quick succession. This sends a bunch of requests to the API for different paginated results. The server may resolve these requests out of order, and the user will see the wrong page of people as a result.

What we want to do is cancel previous API requests, and only permit the most resent one. We can do this using AbortController.

Store the AbortController

First, we’ll need to add a property to hold the AbortController in our state.

export const usePeopleStore = defineStore('people', {
state: () => ({
...
controller: new AbortController(),
}),
...
}

Abort Previous Requests

Next, we’ll want to check to see if the a previous request was sent to the API, and if so, cancel it. We can do this by looking at the ‘loading’ flag.

export const usePeopleStore = defineStore('people', {
...
async getPeople(options = '') {
if (this.loading) {
this.controller.abort();
}
this.controller = new AbortController();

this.loading = true;
...
}
});

Note that we have to create a new AbortController with every request. This is so we get a new instance with a new signal reference.

Pass the Signal With the Request

Now we need to pass our controller’s signal in with the request.

export const usePeopleStore = defineStore('people', {
...
async getPeople(options = '') {
...
await api
.get('/api/people/' + options, {
signal: this.controller.signal,
})
...
}
});

Ignore the Error

When we cancel a request, it is going to throw a CancelError. We can ignore this error since we’re causing it on purpose and it’s what we want to happen. No need to show this to the user.

export const usePeopleStore = defineStore('people', {
...
async getPeople(options = '') {
...
await api
...
.catch((error) => {
if (error.name !== 'CanceledError') {
this.error = error;
console.error(error);
this.loading = false;
}
});
}
});

Our People store should now look something like this.

import { defineStore } from 'pinia';
import { api } from 'src/boot/axios';
import { Person } from 'src/models/';

export const usePeopleStore = defineStore('people', {
state: () => ({
people: [] as Array<Person>,
error: '' as string,
loading: false as boolean,
controller: new AbortController(),
}),
getters: {},
actions: {
async getPeople(options = '') {
if (this.loading) {
this.controller.abort();
}
this.controller = new AbortController();
this.loading = true;

await api
.get('/api/people/' + options, {
signal: this.controller.signal,
})
.then((response) => {
this.people = response.data;
this.error = '';
this.loading = false;
})
.catch((error) => {
if (error.name !== 'CanceledError') {
this.error = error;
console.error(error);
this.loading = false;
}
});
}
}
});

Conclusion

The new AbortController is a standard and widely support JavaScript interface for canceling requests. By properly handling requests in our Pinia stores, we ensure the correct data is always presented to the user.

--

--

Brenton Klik
Brenton Klik

Written by Brenton Klik

Brenton Klik is an Interaction Designer in the Washington DC area. He designs interfaces and experiences for web, mobile, and desktop platforms.

Responses (1)