Search⌘ K
AI Features

Request Cancelation and Abortable Function

Explore techniques for managing request cancellation and abortable functions within the Vue API layer. Understand how to handle asynchronous operations to prevent outdated responses and improve user experience, using axios cancel tokens and abortable functions in scalable applications.

Request cancelation

There are scenarios in which it’s a good idea to cancel a request. A good example is the autocomplete functionality.

Scenario

If we have a massive database, search queries can take a moment before a response is sent back from the server. Imagine a user is trying to search for a meal, and an API call is made any time a user types something into the search input. When a response is received, a list of meals is immediately displayed. A user types a few characters, and two API requests are made to search for a meal. The first request is sent with the query, “la”, whilst the second was with “lasagne.” However, suppose the first API request has finished after the second one. In such a scenario, the search results won’t display data for the latest search, but the old one instead. That’s not a great user experience. We can solve this problem by ensuring that the first request is canceled if the second request is made.

How to cancel the request

So far, we’ve been using the axios library as an HTTP client. Requests can be canceled with axios by creating a cancel token that must be passed to an API request in the config object. However, we don’t want to start creating cancel tokens directly around the application because it would go against one of the API layers’ main principles. That is, an application doesn’t care about the internal implementation of the API layer. Therefore, we have to make sure that the cancellation is made in such a way that it’d be easy to replace it if an application switched to the fetch method or any other HTTP client.

How to implement it?

Two examples show how this can be accomplished. The first tackles the problem from outside of the API layer, whilst the second does so from inside. For the former, we’ll have an abortable function that injects a cancel token into an API function (imported from one of the API files). The abortable function returns an abort method and a wrapped API function with a cancel token injected. For the latter example, we’ll take advantage of the API wrapper around Axios. Instead of using abortable wrapping functions, we’ll initialize a cancel token by passing an abort property as part of the config object.

Abortable function

Let’s implement a feature to allow a user to search for meals. We’ll use the mealdb API. First, let’s start by adding three functions to the base api.js file.

Javascript (babel-node)
// api/api.js
// ...other functions
export const didAbort = error => axios.isCancel(error);
export const getCancelSource = () => axios.CancelToken.source()
export const abortable = fn => {
// Create cancel token and cancel method
const { cancel, token } = getCancelSource()
// Return the cancel method and the wrapped function with a cancel token return {
abort: cancel,
fn: (...args) => {
// If the last argument is not an object then throw
if (typeof args[args.length - 1] !== "object") {
throw new Error("The last argument must be a config object!");
}
// Add the cancel token to the last argument passed
// The last argument passed should always be a config object
args[args.length - 1] = {
...args[args.length - 1],
cancelToken: token,
};
return fn(...args);
},
};
};

Here’s a step-by-step explanation of the cancelation logic:

  • The didAbort function returns a boolean if an error object passed is an instance of a Cancel error thrown by axios.
  • The getCancelSource creates a new cancel source that contains a cancel method and a token, which has to be passed to a request.
  • The abortable function is responsible for injecting the cancel token into the last argument passed to the request function. The one caveat here is that a config object must always be passed to an API function, even if it’s empty. In the abortable function, we don’t know what kind of request is being made, so we can’t determine which one should be a config object based on the number of parameters. Therefore, we must assume that a config object is always passed as the last argument.

We also need a new mealApi.js file that returns the searchMeals method.

Javascript (babel-node)
//api/mealApi.js
import api from "./api";
import { requiredParam } from "@/helpers/requiredParam";
const URLS = {
getMeal: "search.php",
};
export const searchMeals = (query, config = requiredParam("config")) => {
return api.get(URLS.getMeal, {
baseURL: "https://www.themealdb.com/api/json/v1/1/",
params: {
s: query,
},
...config,
});
};

We’ve used a little helper called requiredParam to ensure that an error will be thrown if the config parameter isn’t present. It’s a trick that takes advantage of default parameters. Instead of specifying a normal value, the requiredParam function is called if the config is undefined. However, if the config param is present, then the requiredParam function isn’t called. Below is the implementation for that function.

Javascript (babel-node)
// helpers/requiredParam.js
export const requiredParam = (param = "") => {
const msg = `Param ${param} is required`;
console.error(msg);
throw new Error(msg);
};

It’s time to create a component to handle search and show results. Don’t forget to add it to our routes config.

The template given below has an input field with a v-model bound to the mealQuery property and a list of meal titles received from the mealdb API.

Template
Script
<!-- views/SearchMealExample.vue -->
<template>
<div class="py-8">
<form class="mb-8">
<fieldset class="flex flex-col">
<label class="mb-4 font-semibold" for="meal">Search meal</label>
<input
class="px-4 py-2 border border-gray-300 rounded-lg"
type="text"
autocomplete="off"
v-model="mealQuery"
id="meal"
/>
</fieldset>
</form>
<div>
<h1 class="font-bold text-2xl mb-2">Meals</h1>
<div v-for="meal of meals" :key="meal.idMeal" class="py-1">
<p>{{ meal.strMeal }}</p>
</div>
</div>
</div>
</template>
Creating a template that has an input field and displays the list of meal titles

The main part of this code is the initSearchMeals method. At the start, it calls an abort function that’s set on the $options object of the Vue instance. The first time the initSearchMeals method is called, no abort method is available. That’s why the optional chaining operator, ”?.”, is used to ensure an error like $options.abort is not a function isn’t thrown. If there’s no abort function to call, the JavaScript engine proceeds with code execution. An API method was passed directly to the withAsync helper in previous examples. This time, it’s first passed to the abortable function. The returned object contains an abort method and a wrapped API function with an injected cancel token. The abort function received is set on the $options object, so it can be called if the initSearchMeals is initialized again. If the request is canceled, we can confirm it bypasses the error object to the didAbort function.

Let’s run the following code and see how the abortable function works.

Note: The code below may take a while to run. When the server starts, go to the app URL to see the output.

<template>
  <div id="nav" class="container mx-auto">
    <router-view />
  </div>
</template>

<style></style>
Search meal example
  • The api/api.js file contains the base API wrapper around the axios library. Other API files should use this API wrapper. It also exports getAbort, didAbort, and abortable methods that can be used to handle aborting requests via the API Layer.
  • The api/mealApi.js file contains the searchMeals method, which makes a search request for meals.
  • The api/constants/apiStatus.js file contains constants for all available API statuses: IDLE, PENDING, SUCCESS, and ERROR.
  • The api/composables/useApi.js file contains the useApi composable, which abstracts the handling of API states and request execution.
  • The SearchMealExample.vue component renders a form that can be used to search for meals. Whenever a user enters anything in the search input, the mealQuery state is updated, and the watcher initializes the initSearchMeals method. This method utilizes helpers from the api.js file to abort requests. When a request is aborted, a toast notification is displayed.

If we’re recreating these examples from scratch, just try to type in the search input quickly. We should see some warnings, but not too many, because the mealdb API usually responds very fast.

In the Companion App that is given below, we should see notification popups.

Note: The code below may take a while to run. When the server starts, go to the app URL to see the output.

<template>
  <div id="app" class="bg-gray-100">
    <GlobalSpinnerProvider>
      <Layout>
        <div id="nav"></div>
        <router-view />
      </Layout>
    </GlobalSpinnerProvider>
  </div>
</template>
<script>
import Layout from '@/layout/Layout'
import GlobalSpinnerProvider from '@/components/common/spinner/GlobalSpinnerProvider.vue'
import { setUser } from '@/services/stateful/userService'

/**
 * Set user name for Managing State / Stateful Services example
 */
setUser({
  name: 'William',
})

export default {
  components: {
    Layout,
    GlobalSpinnerProvider,
  },
}
</script>
<style lang="scss" src="@/styles/index.scss"></style>
Request cancelation using Axios library in the companion app
  • The api/api.js file contains the base API wrapper around the axios library. Other API files should use this API wrapper. It’s enhanced with withAbort, which provides request cancellation functionality.

  • The api/quoteApi.js file contains the fetchRandomQuote method.

  • The AbortingRequests.vue component renders some text and a quote. The abort logic is utilized by providing an abort property with a function as part of the config passed to an API method, fetchRandomQuote. If a request is aborted, a toast notification is shown. The toggle switch can be used to control whether requests should be aborted or not.