A couple of days ago in my current working project, we decided to rewrite our simple fetch service to a more powerful solution with the ability to cancel requests which were sent previously. From the beginning, we decided to use axios
library and TypeScript
, as everybody has experience with it previously, and it provides a solution for request canceling based on Promises. It’s already working but will be extended in nearly future.
So, let’s start. Firstly, we need to create an Axios service. All our services in the current project is a class-based, so we need to create a class AxiosService
. There are 2 base approaches of how we can use Axios: directly use axios
object from import or create a new instance with axios.create
. We will use the last one. Service will have 2 fields: instance which has a type AxiosInstance
and cancelToken
which has a type CancelTokenStatic
:
class AxiosService {
instance: AxiosInstance;
cancelTokenStatic: CancelTokenStatic;
}
Then in the constructor, we create axiosInstanse
:
constructor(url: string) {
this.instance = axios.create({
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
baseURL: url
});
this.cancelTokenStatic = axios.CancelToken;
}
axios.create
function can take an object with options. In our case, we set field headers
and told that our instance will work with json
and provide a base URL address that will be used for every request. axios.CancelToken
provide cancel requests logic, so we also save it
One more thing that we need is to create a public function that will update baseURL
. We need it for some specific business cases:
setBaseURL(url: string): void {
this.instance.defaults.baseURL = url;
}
Full version of AxiosService
class:
import axios, { AxiosInstance, CancelTokenStatic } from 'axios';
class AxiosService {
instance: AxiosInstance;
cancelTokenStatic: CancelTokenStatic;
constructor(url: string) {
this.instance = axios.create({
transformRequest: [
(data, headers): string => {
headers['Authorization'] = `Bearer ${authService.getToken()}`;
return JSON.stringify(data);
},
],
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
baseURL: url,
});
this.cancelTokenStatic = axios.CancelToken;
}
setBaseURL(url: string): void {
this.instance.defaults.baseURL = url;
}
}
The next step is to create HttpService
which will implement all other logic with requests. Why do we need to create a new service instead of just adding request logic to AxiosService? Because it will help us to use any other rest library instead of Axios if we want to do this in the future.
HttpService will have 3 fields:
class HttpService {
private axiosInstance: AxiosInstance;
private sourceMap: { [key: string]: CancelTokenSource[] } = {};
private cancelToken: CancelTokenStatic;
}
axiosInstance
and cancelToken
we will set in constructor, sourceMap
will be our store where we will keep cancel tokens for requests
constructor(instance: AxiosInstance, cancelToken: CancelTokenStatic) {
this.axiosInstance = instance;
this.cancelToken = cancelToken;
}
Now let’s implement the logic for canceling requests.
private createCancelToken = (cancelKey: string): CancelToken => {
const source = this.cancelToken.source();
if (this.sourceMap[cancelKey]) {
this.sourceMap[cancelKey].push(source);
} else {
this.sourceMap[cancelKey] = [source];
}
return source.token;
};
Private function createCancelToken
firstly create a new cancel token source. Then we check if our sourceMap
already have a field with key cancelKey
, then we just add new cancelSource
to array or we create a new field in sourceMap
and the source is the first element in the array.
The next step is to create cancelPreviousRequests
method.
cancelPreviousRequests = (cancelKey: string): void => {
const requests = this.sourceMap[cancelKey] || [];
requests.forEach((item) => {
item.cancel('cancel');
});
this.sourceMap[cancelKey] = [];
};
We take a cancelKey
from the argument and find such field in sourceMap
. Then we iterate through every CancelTokenSource
in it and call the cancel
function. And then delete the target field in sourceMap
via saving an empty array to it.
Now we can start with requests logic. We will use the general method sendRequest
for all requests. This method will take some options. Let’s define interface for them:
interface RequestOptions {
url: string; // url for request
method?: Method; // HTTP method
params?: Dictionary<any> | string; // request params
cancelKey?: string; // key for sourceMap if request can be canceled
responseType?: ResponseType; // type for response
}
And the sendRequest
method which will return Promise:
sendRequest<T = unknown>({
url,
method = 'GET',
params,
cancelKey,
responseType,
}: RequestOptions): Promise<AxiosResponse<T> | void> {
let cancelToken;
if (cancelKey) {
cancelToken = this.createCancelToken(cancelKey);
}
switch (method) {
case 'POST':
return this.postRequest({ url, data: params, cancelToken });
default:
return this.getRequest({
url,
cancelToken,
responseType,
});
}
}
We use generic for returned type for response and method we set to GE
T by default. Firstly, we check if we have a cancelKey
, then we need to create cancelToken
and push it to sourceMap
. Then we use switch construction to iterate through the request’s methods and call corresponding functions.
The request function also will take some params, so we need to define an interface for them:
type RequestParams = {
url: string;
cancelToken?: CancelToken;
data?: Dictionary<any> | string;
};
We have 2 base HTTP methods for POST and GET requests:
private async postRequest({
url,
data,
cancelToken,
}: RequestParams): Promise<AxiosResponse | void> {
try {
return await this.axiosInstance.post(url, data, { cancelToken });
} catch (error) {
return Promise.reject(error);
}
}
private async getRequest({
url,
cancelToken,
}: RequestParams): Promise<AxiosResponse | void> {
try {
return await this.axiosInstance.get(url, { cancelToken });
} catch (error) {
return Promise.reject(error);
}
}
Nothing special here. We just call the corresponding request from axiosInstance
with our params
Full code for HttpService:
import {
AxiosResponse,
Method,
CancelTokenSource,
CancelToken,
AxiosInstance,
CancelTokenStatic,
} from 'axios';
import { axiosService } from './AxiosService';
interface RequestOptions {
url: string;
method?: Method;
params?: Dictionary<any> | string;
cancelKey?: string;
}
interface RequestParams {
url: string;
cancelToken?: CancelToken;
data?: Dictionary<any> | string;
}
class HttpService {
private axiosInstance: AxiosInstance;
private sourceMap: { [key: string]: CancelTokenSource[] } = {};
private cancelToken: CancelTokenStatic;
constructor(instance: AxiosInstance, cancelToken: CancelTokenStatic) {
this.axiosInstance = instance;
this.cancelToken = cancelToken;
}
private createCancelToken = (cancelKey: string): CancelToken => {
const source = this.cancelToken.source();
if (this.sourceMap[cancelKey]) {
this.sourceMap[cancelKey].push(source);
} else {
this.sourceMap[cancelKey] = [source];
}
return source.token;
};
cancelPreviousRequests = (cancelKey: string): void => {
const requests = this.sourceMap[cancelKey] || [];
requests.forEach((item) => {
item.cancel('cancel');
});
this.sourceMap[cancelKey] = [];
};
private async postRequest({
url,
data,
cancelToken,
}: RequestParams): Promise<AxiosResponse | void> {
try {
return await this.axiosInstance.post(url, data, { cancelToken });
} catch (error) {
return Promise.reject(error);
}
}
private async getRequest({ url, cancelToken }: RequestParams): Promise<AxiosResponse | void> {
try {
return await this.axiosInstance.get(url, { cancelToken });
} catch (error) {
return Promise.reject(error);
}
}
sendRequest<T = unknown>({
url,
method = 'GET',
params,
cancelKey,
}: RequestOptions): Promise<AxiosResponse<T> | void> {
let cancelToken;
if (cancelKey) {
cancelToken = this.createCancelToken(cancelKey);
}
switch (method) {
case 'POST':
return this.postRequest({ url, data: params, cancelToken });
default:
return this.getRequest({
url,
cancelToken,
});
}
}
Example of usage:
httpService.cancelPreviousRequests('testRequest');
httpService
.sendRequest<string[]>({
url: '/api/testurl',
cancelKey: 'testRequest',
})
.then((response) => {
if (response?.data) {
console.log(response.data)
}
})
.catch((error) => {
console.warn(error);
});