/** * useApiWithErrorHandling - Automatic API Error Integration * * Provides a wrapper hook that automatically: * - Shows errors in ErrorContext * - Handles network errors gracefully * - Provides retry functionality * - Tracks loading states */ import { useCallback, useState, useRef } from 'react'; import { ApiResponse, ApiError } from '@/types'; import { AppError } from '@/types/errors'; import { createErrorFromApi, createNetworkError } from '@/services/errorHandler'; import { useShowErrorSafe } from '@/contexts/ErrorContext'; import { isNetworkError, isTimeoutError, toApiError } from '@/utils/networkErrorRecovery'; /** * Options for API call execution */ interface ApiCallOptions { /** Show error in ErrorContext (default: true) */ showError?: boolean; /** Auto-dismiss error after timeout (default: based on severity) */ autoDismiss?: boolean; /** Custom error message override */ errorMessage?: string; /** Context data for error logging */ context?: Record; /** Callback on retry */ onRetry?: () => void; /** Callback on error dismiss */ onDismiss?: () => void; /** Callback on success */ onSuccess?: () => void; /** Callback on error */ onError?: (error: AppError) => void; } /** * Result from useApiCall hook */ interface UseApiCallResult { /** Execute the API call */ execute: () => Promise>; /** Loading state */ isLoading: boolean; /** Error from last call (if any) */ error: AppError | null; /** Data from last successful call */ data: T | null; /** Reset state */ reset: () => void; /** Retry last call */ retry: () => Promise>; } /** * Hook for executing API calls with automatic error handling * * @param apiCall - Function that returns ApiResponse * @param options - Configuration options * @returns Object with execute function, state, and utilities * * @example * function MyComponent() { * const { execute, isLoading, error, data } = useApiCall( * () => api.getBeneficiary(id), * { onSuccess: () => console.log('Loaded!') } * ); * * useEffect(() => { * execute(); * }, []); * * if (isLoading) return ; * if (error) return ; * return ; * } */ export function useApiCall( apiCall: () => Promise>, options: ApiCallOptions = {} ): UseApiCallResult { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [data, setData] = useState(null); const showError = useShowErrorSafe(); const lastCallRef = useRef(apiCall); // Update ref when apiCall changes lastCallRef.current = apiCall; const execute = useCallback(async (): Promise> => { setIsLoading(true); setError(null); try { const response = await lastCallRef.current(); if (response.ok && response.data !== undefined) { setData(response.data); options.onSuccess?.(); return response; } // Handle API error const apiError = response.error || { message: options.errorMessage || 'Request failed' }; const appError = createErrorFromApi(apiError, options.context); setError(appError); options.onError?.(appError); // Show error in ErrorContext if enabled if (options.showError !== false && showError) { showError(appError, { autoDismiss: options.autoDismiss, onRetry: options.onRetry, onDismiss: options.onDismiss, }); } return response; } catch (err) { // Handle exceptions (network errors, etc.) let appError: AppError; if (isTimeoutError(err)) { appError = createNetworkError('Request timed out. Please try again.', true); } else if (isNetworkError(err)) { appError = createNetworkError('Network error. Please check your connection.'); } else { const apiError = toApiError(err); appError = createErrorFromApi(apiError, options.context); } setError(appError); options.onError?.(appError); // Show error in ErrorContext if enabled if (options.showError !== false && showError) { showError(appError, { autoDismiss: options.autoDismiss ?? true, onRetry: options.onRetry, onDismiss: options.onDismiss, }); } return { ok: false, error: toApiError(err) }; } finally { setIsLoading(false); } }, [options, showError]); const reset = useCallback(() => { setIsLoading(false); setError(null); setData(null); }, []); const retry = useCallback(() => { return execute(); }, [execute]); return { execute, isLoading, error, data, reset, retry, }; } /** * Hook for executing multiple API calls with unified error handling * * @example * const { executeAll, isLoading, errors } = useMultipleApiCalls(); * * const results = await executeAll([ * () => api.getBeneficiaries(), * () => api.getProfile(), * ]); */ export function useMultipleApiCalls() { const [isLoading, setIsLoading] = useState(false); const [errors, setErrors] = useState([]); const showError = useShowErrorSafe(); const executeAll = useCallback( async ( calls: Array<() => Promise>>, options: ApiCallOptions = {} ): Promise>> => { setIsLoading(true); setErrors([]); try { const results = await Promise.all(calls.map(call => call())); const newErrors: AppError[] = []; results.forEach((result, index) => { if (!result.ok && result.error) { const appError = createErrorFromApi(result.error, { ...options.context, callIndex: index, }); newErrors.push(appError); } }); if (newErrors.length > 0) { setErrors(newErrors); // Show first error in ErrorContext if (options.showError !== false && showError && newErrors[0]) { showError(newErrors[0], { autoDismiss: options.autoDismiss, }); } options.onError?.(newErrors[0]); } else { options.onSuccess?.(); } return results; } catch (err) { const appError = createNetworkError( err instanceof Error ? err.message : 'Multiple requests failed' ); setErrors([appError]); if (options.showError !== false && showError) { showError(appError, { autoDismiss: true }); } options.onError?.(appError); return calls.map(() => ({ ok: false, error: toApiError(err) })); } finally { setIsLoading(false); } }, [showError] ); const reset = useCallback(() => { setIsLoading(false); setErrors([]); }, []); return { executeAll, isLoading, errors, hasErrors: errors.length > 0, reset, }; } /** * Hook for mutation operations (create, update, delete) * Provides optimistic updates and rollback on error * * @example * const { mutate, isLoading } = useMutation( * (name: string) => api.updateBeneficiary(id, { name }), * { * onSuccess: () => toast.success('Updated!'), * onError: (error) => console.error(error), * } * ); * * const handleSave = async () => { * await mutate('New Name'); * }; */ export function useMutation( mutationFn: (variables: TVariables) => Promise>, options: ApiCallOptions & { /** Called before mutation starts */ onMutate?: (variables: TVariables) => void | Promise; /** Called when mutation succeeds */ onSuccess?: (data: TData, variables: TVariables) => void | Promise; /** Called when mutation fails */ onError?: (error: AppError, variables: TVariables) => void | Promise; /** Called after mutation completes (success or error) */ onSettled?: (data: TData | undefined, error: AppError | null, variables: TVariables) => void | Promise; } = {} ) { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [data, setData] = useState(null); const showError = useShowErrorSafe(); const mutate = useCallback( async (variables: TVariables): Promise> => { setIsLoading(true); setError(null); try { // Call onMutate for optimistic updates await options.onMutate?.(variables); const response = await mutationFn(variables); if (response.ok && response.data !== undefined) { setData(response.data); await options.onSuccess?.(response.data, variables); await options.onSettled?.(response.data, null, variables); return response; } // Handle error const apiError = response.error || { message: 'Mutation failed' }; const appError = createErrorFromApi(apiError, options.context); setError(appError); await options.onError?.(appError, variables); await options.onSettled?.(undefined, appError, variables); // Show error if enabled if (options.showError !== false && showError) { showError(appError, { autoDismiss: options.autoDismiss, onRetry: () => mutate(variables), }); } return response; } catch (err) { const appError = createNetworkError( err instanceof Error ? err.message : 'Mutation failed' ); setError(appError); await options.onError?.(appError, variables); await options.onSettled?.(undefined, appError, variables); if (options.showError !== false && showError) { showError(appError, { autoDismiss: true }); } return { ok: false, error: toApiError(err) }; } finally { setIsLoading(false); } }, [mutationFn, options, showError] ); const reset = useCallback(() => { setIsLoading(false); setError(null); setData(null); }, []); return { mutate, isLoading, error, data, reset, isError: error !== null, isSuccess: data !== null && error === null, }; } // Export all hooks export default useApiCall;