use-fetch-react-xhr
Version:
A custom hook that's alternative to axios to simplify making api calls for React Application and it also lets you cancel request while it's in progress
305 lines (277 loc) • 7.13 kB
text/typescript
import { useState, useEffect } from "react";
export type ApiParams = {
url: string;
payload?: any;
type?: "GET" | "POST" | "PUT" | "DELETE";
headers?: Record<string, string>;
autoLoad?: boolean;
callOnMount?: boolean;
};
export type ApiCallResult = {
status: "idle" | "loading" | "success" | "error" | "cancelled";
data?: any;
error?: any;
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
isCancelled: boolean;
request: XMLHttpRequest | null;
};
export type UseFetchResponse = {
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
data: any;
error: APIError;
isCancelled: boolean;
load: () => void;
cancel: () => void;
request: XMLHttpRequest | null;
};
type ApiCallOptions = {
onSuccess?: (data: any) => void;
onError?: (error: any) => void;
onCancel?: () => void;
};
type StartFunction = (
url: string,
type?: string,
payload?: any,
headers?: Record<string, string>
) => void;
type RequestParams = {
url: string;
type?: string;
payload?: any;
headers?: Record<string, string>;
};
const MIN_REQUEST_DELAY = 50;
export const createApiCallFunction = () => {
let listener: any = null;
let request: XMLHttpRequest | any;
let isCancelled = false;
let lastRequestParams: RequestParams | undefined;
let lastRequestTime = 0;
let requestStatus = {
status: "idle",
isLoading: false,
isSuccess: false,
isError: false,
isCancelled: false,
};
const notifyListener = (result: ApiCallResult) => {
if (result) {
requestStatus = result;
}
if (listener) {
listener(requestStatus);
}
};
const start: StartFunction = (
url,
type = "GET",
payload,
headers = { "content-type": "application/json" }
) => {
const now = Date.now();
const requestParams: RequestParams = { url, type, payload, headers };
if (
request &&
now - lastRequestTime < MIN_REQUEST_DELAY &&
isEqual(requestParams, lastRequestParams)
) {
// Ignore this request
return;
}
lastRequestParams = requestParams;
lastRequestTime = now;
isCancelled = false;
request = new XMLHttpRequest();
request.open(type, url, true);
if (headers) {
for (const header in headers) {
request.setRequestHeader(header, headers[header]);
}
}
request.onreadystatechange = () => {
if (isCancelled) {
return;
}
if (request.readyState === 4) {
try {
if (request.status >= 200 && request.status < 300) {
const response = JSON.parse(request.responseText);
// throw Error('new error');
notifyListener({
status: "success",
data: response,
isLoading: false,
isSuccess: true,
isError: false,
request,
error: null,
isCancelled: false,
});
} else {
let data = "";
if (
request
.getResponseHeader("Content-Type")
.includes("application/json")
) {
data = JSON.parse(request.responseText);
}
const error = {
status: request.status,
statusText: request.statusText,
data: data,
message: "Error Loading Data!",
};
notifyListener({
status: "error",
error,
request,
isLoading: false,
isSuccess: false,
isError: true,
isCancelled: false,
});
}
} catch (error) {
notifyListener({
status: "error",
error: {
status: request.status,
statusText: request.statusText,
data: {},
message: "error while parsing response from server" + error,
},
request,
isLoading: false,
isSuccess: false,
isError: true,
isCancelled: false,
});
}
}
};
// setTimeout(() => {
request.send(
headers["content-type"] === "application/json"
? JSON.stringify(payload)
: payload
);
notifyListener({
status: "loading",
isLoading: true,
isSuccess: false,
isError: false,
isCancelled: false,
request,
});
// }, MIN_REQUEST_DELAY);
};
const cancel = () => {
if (request && requestStatus.isLoading) {
request.abort();
isCancelled = true;
notifyListener({
status: "cancelled",
isLoading: false,
isSuccess: false,
isError: false,
isCancelled: true,
request,
});
}
};
const removeListener = () => {
listener = null;
};
const onChange = (newListener: any) => {
listener = newListener;
};
return {
onChange,
start,
cancel,
removeListener,
};
};
function isEqual(a: any, b: any) {
return JSON.stringify(a) === JSON.stringify(b);
}
export type APIError = {
status: number;
statusText: string;
message: any;
data: any;
};
export const useFetch = ({
url,
payload,
headers,
type,
autoLoad = false,
callOnMount = false,
}: ApiParams): UseFetchResponse => {
const [result, setResult] = useState<ApiCallResult>({
status: "idle",
data: null,
error: null,
isLoading: false,
isSuccess: false,
isError: false,
isCancelled: false,
request: null,
});
const [apiCall, setAPICall] = useState<any>(null);
const [makeInitialCall, setMakeInitialCall] = useState(callOnMount);
useEffect(() => {
setAPICall(createApiCallFunction());
}, []);
useEffect(() => {
if (apiCall) {
const onChange = (apiResult: ApiCallResult) => setResult(apiResult);
apiCall.onChange(onChange);
if (makeInitialCall) {
load();
}
return () => {
apiCall?.removeListener(onChange);
cancel();
};
}
}, [apiCall]);
useEffect(() => {
if (apiCall && autoLoad) {
cancel();
load();
return () => {
result.isLoading && apiCall.cancel();
};
}
}, [url, payload, headers, type]);
const load = () => {
if (apiCall) {
cancel();
apiCall.start(url, type, payload, headers);
} else {
setMakeInitialCall(true);
}
};
const cancel = () => {
result.isLoading && apiCall?.cancel();
};
return {
isLoading: result.isLoading || false,
isSuccess: result.isSuccess || false,
isError: result.isError || false,
data: result.data || null,
error: result.error || "",
isCancelled: result.isCancelled || false,
load,
cancel,
request: result.request,
};
};