/** * useError - Hook for error handling with recovery strategies * * Features: * - Handle API errors consistently * - Automatic retry with exponential backoff * - User-friendly error display * - Error state management */ import { useState, useCallback, useRef } from 'react'; import { Alert } from 'react-native'; import { AppError, ErrorState, initialErrorState, isRetryableError, } from '@/types/errors'; import { createErrorFromApi, createErrorFromUnknown, calculateRetryDelay, logError, } from '@/services/errorHandler'; import { ApiError, ApiResponse } from '@/types'; import { useShowErrorSafe } from '@/contexts/ErrorContext'; interface UseErrorOptions { // Show error in global toast showToast?: boolean; // Show native alert showAlert?: boolean; // Custom alert title alertTitle?: string; // Auto-retry on failure autoRetry?: boolean; // Max retry attempts (overrides error default) maxRetries?: number; // Callback when error occurs onError?: (error: AppError) => void; // Callback when retry starts onRetryStart?: (attempt: number) => void; // Callback when all retries exhausted onRetryExhausted?: (error: AppError) => void; } interface UseErrorReturn { // Current error state errorState: ErrorState; // Set error from API response setApiError: (error: ApiError, context?: Record) => void; // Set error from any value setError: (error: unknown, context?: Record) => void; // Clear current error clearError: () => void; // Clear all errors clearAllErrors: () => void; // Handle API response (returns data if ok, sets error if not) handleApiResponse: ( response: ApiResponse, context?: Record ) => T | null; // Wrap async function with error handling wrapAsync: ( fn: () => Promise, context?: Record ) => Promise; // Execute with retry executeWithRetry: ( fn: () => Promise>, context?: Record ) => Promise; // Check if currently has error hasError: boolean; // Current error (if any) error: AppError | null; } export function useError(options: UseErrorOptions = {}): UseErrorReturn { const { showToast = true, showAlert = false, alertTitle = 'Error', autoRetry = false, maxRetries, onError, onRetryStart, onRetryExhausted, } = options; const [errorState, setErrorState] = useState(initialErrorState); const retryCountRef = useRef>(new Map()); // Get global show error function (may not be available if not in ErrorProvider) const showGlobalError = useShowErrorSafe(); // Internal function to handle error const handleError = useCallback( (appError: AppError, retryFn?: () => void) => { // Log error logError(appError); // Update state setErrorState({ hasError: true, error: appError, errors: [appError], lastErrorAt: new Date(), }); // Call callback onError?.(appError); // Show toast if (showToast && showGlobalError) { showGlobalError(appError, { onRetry: retryFn }); } // Show alert if (showAlert) { const buttons: { text: string; onPress?: () => void; style?: 'cancel' | 'default' | 'destructive' }[] = [ { text: 'OK', style: 'cancel' as const }, ]; if (isRetryableError(appError) && retryFn) { buttons.unshift({ text: 'Retry', onPress: retryFn, }); } Alert.alert(alertTitle, appError.userMessage, buttons); } }, [showToast, showAlert, alertTitle, onError, showGlobalError] ); // Set error from API error const setApiError = useCallback( (error: ApiError, context?: Record) => { const appError = createErrorFromApi(error, context); handleError(appError); }, [handleError] ); // Set error from any value const setError = useCallback( (error: unknown, context?: Record) => { const appError = createErrorFromUnknown(error, undefined, context); handleError(appError); }, [handleError] ); // Clear current error const clearError = useCallback(() => { setErrorState(initialErrorState); }, []); // Clear all errors const clearAllErrors = useCallback(() => { setErrorState(initialErrorState); retryCountRef.current.clear(); }, []); // Handle API response const handleApiResponse = useCallback( (response: ApiResponse, context?: Record): T | null => { if (response.ok && response.data !== undefined) { clearError(); return response.data; } if (response.error) { setApiError(response.error, context); } return null; }, [clearError, setApiError] ); // Wrap async function with error handling const wrapAsync = useCallback( async ( fn: () => Promise, context?: Record ): Promise => { try { const result = await fn(); clearError(); return result; } catch (error) { setError(error, context); return null; } }, [clearError, setError] ); // Execute with retry const executeWithRetry = useCallback( async ( fn: () => Promise>, context?: Record ): Promise => { const operationId = Date.now().toString(); let attempt = 0; const maxAttempts = maxRetries ?? 3; const execute = async (): Promise => { try { const response = await fn(); if (response.ok && response.data !== undefined) { clearError(); retryCountRef.current.delete(operationId); return response.data; } if (response.error) { const appError = createErrorFromApi(response.error, context); // Check if should retry const shouldRetry = autoRetry && isRetryableError(appError) && attempt < maxAttempts; if (shouldRetry) { attempt++; onRetryStart?.(attempt); const delay = calculateRetryDelay(appError.retry, attempt - 1); await new Promise((resolve) => setTimeout(resolve, delay)); return execute(); } // No more retries if (attempt > 0) { onRetryExhausted?.(appError); } handleError(appError, autoRetry ? () => execute() : undefined); } return null; } catch (error) { const appError = createErrorFromUnknown(error, undefined, context); // Check if should retry const shouldRetry = autoRetry && isRetryableError(appError) && attempt < maxAttempts; if (shouldRetry) { attempt++; onRetryStart?.(attempt); const delay = calculateRetryDelay(appError.retry, attempt - 1); await new Promise((resolve) => setTimeout(resolve, delay)); return execute(); } // No more retries if (attempt > 0) { onRetryExhausted?.(appError); } handleError(appError, autoRetry ? () => execute() : undefined); return null; } }; return execute(); }, [autoRetry, maxRetries, clearError, handleError, onRetryStart, onRetryExhausted] ); return { errorState, setApiError, setError, clearError, clearAllErrors, handleApiResponse, wrapAsync, executeWithRetry, hasError: errorState.hasError, error: errorState.error, }; } /** * useFieldErrors - Hook for managing field-level validation errors */ interface FieldErrors { [field: string]: string | undefined; } interface UseFieldErrorsReturn { errors: FieldErrors; setFieldError: (field: string, message: string) => void; clearFieldError: (field: string) => void; clearAllFieldErrors: () => void; hasErrors: boolean; getError: (field: string) => string | undefined; getErrorList: () => { field: string; message: string }[]; setErrors: (errors: FieldErrors) => void; } export function useFieldErrors(): UseFieldErrorsReturn { const [errors, setErrors] = useState({}); const setFieldError = useCallback((field: string, message: string) => { setErrors((prev) => ({ ...prev, [field]: message })); }, []); const clearFieldError = useCallback((field: string) => { setErrors((prev) => { const { [field]: _, ...rest } = prev; return rest; }); }, []); const clearAllFieldErrors = useCallback(() => { setErrors({}); }, []); const getError = useCallback( (field: string) => errors[field], [errors] ); const getErrorList = useCallback(() => { return Object.entries(errors) .filter(([_, message]) => message !== undefined) .map(([field, message]) => ({ field, message: message as string })); }, [errors]); const hasErrors = Object.values(errors).some((v) => v !== undefined); return { errors, setFieldError, clearFieldError, clearAllFieldErrors, hasErrors, getError, getErrorList, setErrors, }; } export default useError;