updates
This commit is contained in:
172
Frontend/src/hooks/useApiCall.ts
Normal file
172
Frontend/src/hooks/useApiCall.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
interface UseApiCallOptions {
|
||||
showSuccessToast?: boolean;
|
||||
successMessage?: string;
|
||||
showErrorToast?: boolean;
|
||||
onSuccess?: (data: any) => void;
|
||||
onError?: (error: any) => void;
|
||||
cancelOnUnmount?: boolean; // Cancel request on component unmount
|
||||
}
|
||||
|
||||
interface UseApiCallReturn<T> {
|
||||
execute: (...args: any[]) => Promise<T | undefined>;
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
clearError: () => void;
|
||||
cancel: () => void; // Manual cancellation function
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for handling API calls with loading states and error handling
|
||||
* Includes request cancellation on component unmount to prevent memory leaks
|
||||
*
|
||||
* @param apiFunction - The async API function to call
|
||||
* @param options - Configuration options
|
||||
* @returns Object with execute function, loading state, error state, clearError function, and cancel function
|
||||
*/
|
||||
export function useApiCall<T = any>(
|
||||
apiFunction: (...args: any[]) => Promise<T>,
|
||||
options: UseApiCallOptions = {}
|
||||
): UseApiCallReturn<T> {
|
||||
const {
|
||||
showSuccessToast = false,
|
||||
successMessage = 'Operation completed successfully',
|
||||
showErrorToast = true,
|
||||
onSuccess,
|
||||
onError,
|
||||
cancelOnUnmount = true, // Default to true for safety
|
||||
} = options;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
// Track component mount status
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
// Cancel pending request on unmount if enabled
|
||||
if (cancelOnUnmount && abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [cancelOnUnmount]);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const execute = useCallback(
|
||||
async (...args: any[]): Promise<T | undefined> => {
|
||||
// Cancel any previous request
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
// Create new AbortController for this request
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Check if component is still mounted before making request
|
||||
if (!mountedRef.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check if apiFunction supports AbortSignal
|
||||
// If the function accepts a signal parameter, pass it
|
||||
let result: T;
|
||||
if (args.length > 0 && typeof args[args.length - 1] === 'object' && 'signal' in args[args.length - 1]) {
|
||||
// Function already has signal in arguments
|
||||
result = await apiFunction(...args);
|
||||
} else {
|
||||
// Try to pass signal as last argument if function supports it
|
||||
try {
|
||||
result = await apiFunction(...args, { signal: abortController.signal });
|
||||
} catch (err: any) {
|
||||
// If function doesn't support signal, try without it
|
||||
if (err.name === 'TypeError' && err.message.includes('signal')) {
|
||||
result = await apiFunction(...args);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if component is still mounted before updating state
|
||||
if (!mountedRef.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Check if request was cancelled
|
||||
if (abortController.signal.aborted) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (showSuccessToast) {
|
||||
toast.success(successMessage);
|
||||
}
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess(result);
|
||||
}
|
||||
|
||||
abortControllerRef.current = null;
|
||||
return result;
|
||||
} catch (err: any) {
|
||||
// Don't handle errors if component is unmounted or request was cancelled
|
||||
if (!mountedRef.current || abortController.signal.aborted || err.name === 'AbortError') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const errorMessage =
|
||||
err?.response?.data?.message ||
|
||||
err?.response?.data?.detail ||
|
||||
err?.message ||
|
||||
'An error occurred. Please try again.';
|
||||
|
||||
const apiError = err instanceof Error ? err : new Error(errorMessage);
|
||||
setError(apiError);
|
||||
|
||||
logger.error('API call failed', err);
|
||||
|
||||
if (showErrorToast && !err.isCancelled) {
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
|
||||
if (onError && !err.isCancelled) {
|
||||
onError(err);
|
||||
}
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
// Only update loading state if component is still mounted
|
||||
if (mountedRef.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
},
|
||||
[apiFunction, showSuccessToast, successMessage, showErrorToast, onSuccess, onError]
|
||||
);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
return { execute, isLoading, error, clearError, cancel };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user