Skip to content

Instantly share code, notes, and snippets.

@Gk0Wk
Created October 21, 2022 03:35
Show Gist options
  • Save Gk0Wk/d25b491c0dbec242d9a71c938a82a349 to your computer and use it in GitHub Desktop.
Save Gk0Wk/d25b491c0dbec242d9a71c938a82a349 to your computer and use it in GitHub Desktop.
Create Api Hooks for React with Axios easily.
import { useEffect, useState, useRef, useCallback } from 'react';
import { cloneDeep, defaultsDeep, isFunction } from 'lodash';
import axios, {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
AxiosError,
Method,
} from 'axios';
export const isPromise = (object: any): boolean =>
typeof object === 'object' && typeof object?.then === 'function';
export const waitIfPromise = async <T = any>(object: T | Promise<T>) =>
isPromise(object) ? await object : object;
export type CallHandler<
CallPropsType = any,
AxiosRequestConfigType = AxiosRequestConfig,
> = (
callProps: CallPropsType,
) => AxiosRequestConfigType | Promise<AxiosRequestConfigType>;
export type ResultHandler<
ResultType = any,
CallPropsType = any,
AxiosResponseType = AxiosResponse,
> = (
response: AxiosResponseType,
callProps: CallPropsType,
) => ResultType | Promise<ResultType>;
export type ErrorHandler<
ErrorType extends Error = Error,
AxiosErrorType = AxiosError,
> = (
axiosError: AxiosErrorType | undefined,
callError: Error | undefined,
resultError: Error | undefined,
) => ErrorType | Promise<ErrorType>;
type ApiReducerHook<
CallPropsType = any,
ResultType = any,
ErrorType extends Error = Error,
> = () => {
result: ResultType | undefined;
error: ErrorType | undefined;
pending: boolean;
idle: boolean;
fullfilled: boolean;
rejected: boolean;
dispatch: (
props: CallPropsType | (() => CallPropsType),
onProgress?: ProgressHandler<ResultType, ErrorType>,
) => void;
cancel: (reason?: any) => void;
};
type ProgressHandler<ResultType = any, ErrorType extends Error = Error> = (
result: ResultType | undefined,
error: ErrorType | undefined,
current: number,
finished: number,
total: number,
) => void;
type ApiBatchReducerHook<
CallPropsType = any,
ResultType = any,
ErrorType extends Error = Error,
> = () => {
results: (ResultType | undefined)[];
errors: (ErrorType | undefined)[];
pending: boolean;
idle: boolean;
finished: boolean;
dispatch: (
props: CallPropsType[] | (() => CallPropsType[]),
onProgress?: ProgressHandler<ResultType, ErrorType>,
) => void;
cancel: (reason?: any) => void;
};
type ApiMemoHook<
CallPropsType = any,
ResultType = any,
ErrorType extends Error = Error,
> = (
props: CallPropsType | (() => CallPropsType),
deps: readonly any[],
) => [
ResultType | undefined,
ErrorType | undefined,
boolean,
(reason?: any) => void,
() => void,
];
type ApiBatchMemoHook<
CallPropsType = any,
ResultType = any,
ErrorType extends Error = Error,
> = (
props: CallPropsType[] | (() => CallPropsType[]),
deps: readonly any[],
) => [
(ResultType | undefined)[],
(ErrorType | undefined)[],
boolean,
(reason?: any) => void,
() => void,
];
export class Api<
CallPropsType = any,
ResultType = any,
ErrorType extends Error = Error,
AxiosRequestConfigType extends AxiosRequestConfig = AxiosRequestConfig,
AxiosResponseType extends AxiosResponse = AxiosResponse,
AxiosErrorType = AxiosError,
> {
protected axiosInstance: AxiosInstance = axios;
protected axiosDefaultConfig: AxiosRequestConfig = {};
protected callHandler!: CallHandler<CallPropsType, AxiosRequestConfigType>;
protected resultHandler!: ResultHandler<
ResultType,
CallPropsType,
AxiosResponseType
>;
protected errorHandler?: ErrorHandler<ErrorType, AxiosErrorType>;
protected beforeRequest?: (
config: AxiosRequestConfigType,
) => void | Promise<void>;
protected afterResponse?: (
response: AxiosResponseType,
) => void | Promise<void>;
constructor(
callHandler: CallHandler<CallPropsType, AxiosRequestConfigType>,
resultHandler: ResultHandler<ResultType, CallPropsType, AxiosResponseType>,
errorHandler?: ErrorHandler<ErrorType, AxiosErrorType>,
) {
this.handleCall(callHandler);
this.handleResult(resultHandler);
if (errorHandler) {
this.handleError(errorHandler);
}
}
setAxiosInstance(axiosInstance: AxiosInstance) {
this.axiosInstance = axiosInstance;
return this;
}
setDefaultAxiosConfig(config: AxiosRequestConfig) {
this.axiosDefaultConfig = cloneDeep(config);
return this;
}
setUrl(url: string) {
this.axiosDefaultConfig.url = url;
return this;
}
setMethod(method: Method) {
this.axiosDefaultConfig.method = method;
return this;
}
handleCall(callHandler: CallHandler<CallPropsType, AxiosRequestConfigType>) {
this.callHandler = callHandler;
return this;
}
handleResult(
resultHandler: ResultHandler<ResultType, CallPropsType, AxiosResponseType>,
) {
this.resultHandler = resultHandler;
return this;
}
handleError(errorHandler: ErrorHandler<ErrorType, AxiosErrorType>) {
this.errorHandler = errorHandler;
return this;
}
setBeforeRequest(
action: (config: AxiosRequestConfigType) => void | Promise<void>,
) {
this.beforeRequest = action;
return this;
}
setAfterResponse(
action: (response: AxiosResponseType) => void | Promise<void>,
) {
this.afterResponse = action;
return this;
}
async call(
props: CallPropsType,
controller?: AbortController,
): Promise<ResultType> {
let config: AxiosRequestConfigType;
try {
config = cloneDeep(await waitIfPromise(this.callHandler(props)));
} catch (e: any) {
console.error(e);
throw await waitIfPromise(
this.errorHandler?.(undefined, e, undefined) ?? e,
);
}
let response: AxiosResponseType;
try {
const finalConfig = defaultsDeep(
config,
this.axiosDefaultConfig,
controller
? {
signal: controller.signal,
}
: {},
);
if (this.beforeRequest) {
await waitIfPromise(this.beforeRequest(finalConfig));
}
response = (await this.axiosInstance(
finalConfig,
)) as unknown as AxiosResponseType;
if (this.afterResponse) {
await waitIfPromise(this.afterResponse(response));
}
} catch (e: any) {
console.error(e);
throw await waitIfPromise(
this.errorHandler?.(e, undefined, undefined) ?? e,
);
}
let result: ResultType;
try {
result = await waitIfPromise(this.resultHandler(response, props));
} catch (e: any) {
console.error(e);
throw await waitIfPromise(
this.errorHandler?.(undefined, undefined, e) ?? e,
);
}
return result;
}
createReducerHook(): ApiReducerHook<CallPropsType, ResultType, ErrorType> {
return () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
const cancelRef = useRef<(reason?: string) => void>(() => {});
const cancel = useCallback((reason?: string) => {
cancelRef.current(reason);
}, []);
const [call, setCall] = useState<{
result: ResultType | undefined;
error: ErrorType | undefined;
pending: boolean;
idle: boolean;
fullfilled: boolean;
rejected: boolean;
}>(() => ({
result: undefined,
error: undefined,
pending: false,
idle: true,
fullfilled: false,
rejected: false,
}));
const dispatch = useCallback(
(
props: CallPropsType | (() => CallPropsType),
onProgress?: ProgressHandler<ResultType, ErrorType>,
) => {
cancelRef.current('New call begins');
const _props = isFunction(props) ? props() : props;
const abortController = new AbortController();
cancelRef.current = (reason?: string) => {
if (!abortController.signal.aborted) {
abortController.abort(reason);
}
};
setCall({
result: undefined,
error: undefined,
pending: true,
idle: false,
fullfilled: false,
rejected: false,
});
this.call(_props, abortController)
.then(result => {
setCall({
result,
error: undefined,
pending: false,
idle: false,
fullfilled: true,
rejected: false,
});
onProgress?.(result, undefined, 0, 1, 1);
})
.catch(error => {
setCall({
result: undefined,
error,
pending: false,
idle: false,
fullfilled: false,
rejected: true,
});
onProgress?.(undefined, error, 0, 1, 1);
});
},
[],
);
return { ...call, dispatch, cancel };
};
}
createBatchReducerHook(
poolSize = 10,
lazyUpdate = true,
): ApiBatchReducerHook<CallPropsType, ResultType, ErrorType> {
return () => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
const cancelRef = useRef<(reason?: string) => void>(() => {});
const cancel = useCallback((reason?: string) => {
cancelRef.current(reason);
}, []);
const [callState, setCallState] = useState<{
idle: boolean;
pending: boolean;
finished: boolean;
results: (ResultType | undefined)[];
errors: (ErrorType | undefined)[];
}>({
idle: true,
pending: false,
finished: false,
results: [],
errors: [],
});
const dispatch = useCallback(
(
propsList: CallPropsType[] | (() => CallPropsType[]),
onProgress?: ProgressHandler<ResultType, ErrorType>,
) => {
cancelRef.current('New call begins');
const _propsList = isFunction(propsList) ? propsList() : propsList;
const abortController = new AbortController();
cancelRef.current = (reason?: string) => {
if (!abortController.signal.aborted) {
abortController.abort(reason);
}
};
const total = _propsList.length;
const stat = {
total,
finished: 0,
next: 0,
};
const pool = new Set<number>();
const results: (ResultType | undefined)[] = Array.from({
length: total,
});
const errors: (ErrorType | undefined)[] = Array.from({
length: total,
});
setCallState({
idle: false,
pending: true,
finished: false,
results: [...results],
errors: [...errors],
});
const pushCall = () => {
while (pool.size < poolSize && stat.next < stat.total) {
const current = stat.next++;
const props = _propsList[current];
pool.add(current);
(async () => {
try {
const result = await this.call(props, abortController);
const finished = ++stat.finished;
onProgress?.(
result,
undefined,
current,
finished,
stat.total,
);
results[current] = result;
} catch (e: any) {
const finished = ++stat.finished;
onProgress?.(undefined, e, current, finished, stat.total);
errors[current] = e;
}
pool.delete(current);
if (!lazyUpdate && stat.finished < stat.total) {
setCallState({
idle: false,
pending: true,
finished: false,
results: [...results],
errors: [...errors],
});
}
pushCall();
})();
}
if (stat.finished === stat.total) {
setCallState({
idle: false,
pending: false,
finished: true,
results: [...results],
errors: [...errors],
});
}
};
pushCall();
},
[],
);
return { ...callState, dispatch, cancel };
};
}
createMemoHook(): ApiMemoHook<CallPropsType, ResultType, ErrorType> {
const reducerHook = this.createReducerHook();
return (props, deps) => {
const { result, error, pending, idle, cancel, dispatch } = reducerHook();
// eslint-disable-next-line @typescript-eslint/no-empty-function
const retryRef = useRef<() => void>(() => {});
const retry = useCallback(() => {
retryRef.current();
}, []);
useEffect(() => {
retryRef.current = () => {
dispatch(props);
};
retryRef.current();
}, deps);
return [result, error, idle || pending, cancel, retry];
};
}
createBatchMemoHook(
poolSize = 10,
lazyUpdate = true,
): ApiBatchMemoHook<CallPropsType, ResultType, ErrorType> {
const reducerHook = this.createBatchReducerHook(poolSize, lazyUpdate);
return (props, deps) => {
const { results, errors, pending, idle, cancel, dispatch } =
reducerHook();
// eslint-disable-next-line @typescript-eslint/no-empty-function
const retryRef = useRef<() => void>(() => {});
const retry = useCallback(() => {
retryRef.current();
}, []);
useEffect(() => {
retryRef.current = () => {
dispatch(props);
};
retryRef.current();
}, deps);
return [results, errors, idle || pending, cancel, retry];
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment