WellNuo/hooks/useError.ts
Sergei a238b7e35f Add comprehensive error handling system
- Add extended error types with severity levels, retry policies,
  and contextual information (types/errors.ts)
- Create centralized error handler service with user-friendly
  message translation and classification (services/errorHandler.ts)
- Add ErrorContext for global error state management with
  auto-dismiss and error queue support (contexts/ErrorContext.tsx)
- Create error UI components: ErrorToast, FieldError,
  FieldErrorSummary, FullScreenError, EmptyState, OfflineState
- Add useError hook with retry strategies and API response handling
- Add useAsync hook for async operations with comprehensive state
- Create error message utilities with validation helpers
- Add tests for errorHandler and errorMessages (88 tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-31 17:43:07 -08:00

369 lines
9.3 KiB
TypeScript

/**
* 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<string, unknown>) => void;
// Set error from any value
setError: (error: unknown, context?: Record<string, unknown>) => void;
// Clear current error
clearError: () => void;
// Clear all errors
clearAllErrors: () => void;
// Handle API response (returns data if ok, sets error if not)
handleApiResponse: <T>(
response: ApiResponse<T>,
context?: Record<string, unknown>
) => T | null;
// Wrap async function with error handling
wrapAsync: <T>(
fn: () => Promise<T>,
context?: Record<string, unknown>
) => Promise<T | null>;
// Execute with retry
executeWithRetry: <T>(
fn: () => Promise<ApiResponse<T>>,
context?: Record<string, unknown>
) => Promise<T | null>;
// 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<ErrorState>(initialErrorState);
const retryCountRef = useRef<Map<string, number>>(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<string, unknown>) => {
const appError = createErrorFromApi(error, context);
handleError(appError);
},
[handleError]
);
// Set error from any value
const setError = useCallback(
(error: unknown, context?: Record<string, unknown>) => {
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(
<T>(response: ApiResponse<T>, context?: Record<string, unknown>): 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 <T>(
fn: () => Promise<T>,
context?: Record<string, unknown>
): Promise<T | null> => {
try {
const result = await fn();
clearError();
return result;
} catch (error) {
setError(error, context);
return null;
}
},
[clearError, setError]
);
// Execute with retry
const executeWithRetry = useCallback(
async <T>(
fn: () => Promise<ApiResponse<T>>,
context?: Record<string, unknown>
): Promise<T | null> => {
const operationId = Date.now().toString();
let attempt = 0;
const maxAttempts = maxRetries ?? 3;
const execute = async (): Promise<T | null> => {
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<FieldErrors>({});
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;