/** * Enhanced Form Hook with Advanced Error Handling * * Provides comprehensive form state management, validation, and error handling * with both user-friendly and developer-friendly error messages. */ import { useState, useCallback, useEffect, FormEvent, ChangeEvent, Dispatch, SetStateAction } from 'react'; import { parseApiError, formatValidationErrorsForForm, logErrorWithContext, createErrorContext } from '@/lib/utils/errorUtils'; import { ApiError } from '@/lib/api/base/ApiError'; export interface FormField { value: T; error?: string; touched: boolean; validating: boolean; } export interface FormState> { fields: { [K in keyof T]: FormField }; isValid: boolean; isSubmitting: boolean; submitError?: string; submitCount: number; } export interface FormOptions> { initialValues: T; validate?: (values: T) => Record | Promise>; onSubmit: (values: T) => Promise; onError?: (error: unknown, values: T) => void; onSuccess?: (values: T) => void; component?: string; } export interface UseEnhancedFormReturn> { formState: FormState; setFormState: Dispatch>>; handleChange: (e: ChangeEvent) => void; setFieldValue: (field: K, value: T[K]) => void; setFieldError: (field: K, error: string) => void; handleSubmit: (e: FormEvent) => Promise; reset: () => void; setFormError: (error: string) => void; clearFieldError: (field: K) => void; validateField: (field: K) => Promise; } /** * Enhanced form hook with comprehensive error handling */ export function useEnhancedForm>( options: FormOptions ): UseEnhancedFormReturn { const [formState, setFormState] = useState>(() => ({ fields: Object.keys(options.initialValues).reduce((acc, key) => ({ ...acc, [key]: { value: options.initialValues[key as keyof T], error: undefined, touched: false, validating: false, } }), {} as { [K in keyof T]: FormField }), isValid: true, isSubmitting: false, submitError: undefined, submitCount: 0, })); // Validate form on change useEffect(() => { if (options.validate && formState.submitCount > 0) { const validateAsync = async () => { try { const errors = await options.validate!(getValues()); setFormState(prev => ({ ...prev, isValid: Object.keys(errors).length === 0, fields: Object.keys(prev.fields).reduce((acc, key) => ({ ...acc, [key]: { ...prev.fields[key as keyof T], error: errors[key], } }), {} as { [K in keyof T]: FormField }), })); } catch (error) { console.error('Validation error:', error); } }; validateAsync(); } }, [formState.fields, formState.submitCount, options.validate]); const getValues = useCallback((): T => { return Object.keys(formState.fields).reduce((acc, key) => ({ ...acc, [key]: formState.fields[key as keyof T].value, }), {} as T); }, [formState.fields]); const handleChange = useCallback((e: ChangeEvent) => { const { name, value, type } = e.target; const checked = 'checked' in e.target ? e.target.checked : false; const fieldValue = type === 'checkbox' ? checked : value; setFormState(prev => ({ ...prev, fields: { ...prev.fields, [name]: { ...prev.fields[name as keyof T], value: fieldValue as T[keyof T], touched: true, error: undefined, // Clear error on change }, }, })); }, []); const setFieldValue = useCallback((field: K, value: T[K]) => { setFormState(prev => ({ ...prev, fields: { ...prev.fields, [field]: { ...prev.fields[field], value, touched: true, error: undefined, }, }, })); }, []); const setFieldError = useCallback((field: K, error: string) => { setFormState(prev => ({ ...prev, fields: { ...prev.fields, [field]: { ...prev.fields[field], error, touched: true, }, }, isValid: false, })); }, []); const clearFieldError = useCallback((field: K) => { setFormState(prev => ({ ...prev, fields: { ...prev.fields, [field]: { ...prev.fields[field], error: undefined, }, }, })); }, []); const setFormError = useCallback((error: string) => { setFormState(prev => ({ ...prev, submitError: error, })); }, []); const validateField = useCallback(async (field: K) => { if (!options.validate) return; setFormState(prev => ({ ...prev, fields: { ...prev.fields, [field]: { ...prev.fields[field], validating: true, }, }, })); try { const values = getValues(); const errors = await options.validate(values); setFormState(prev => ({ ...prev, fields: { ...prev.fields, [field]: { ...prev.fields[field], error: errors[field as string], validating: false, }, }, })); } catch (error) { setFormState(prev => ({ ...prev, fields: { ...prev.fields, [field]: { ...prev.fields[field], validating: false, }, }, })); } }, [options.validate, getValues]); const reset = useCallback(() => { setFormState({ fields: Object.keys(options.initialValues).reduce((acc, key) => ({ ...acc, [key]: { value: options.initialValues[key as keyof T], error: undefined, touched: false, validating: false, } }), {} as { [K in keyof T]: FormField }), isValid: true, isSubmitting: false, submitError: undefined, submitCount: 0, }); }, [options.initialValues]); const handleSubmit = useCallback(async (e: FormEvent) => { e.preventDefault(); const values = getValues(); // Increment submit count to trigger validation setFormState(prev => ({ ...prev, submitCount: prev.submitCount + 1, isSubmitting: true, submitError: undefined, })); // Run validation if provided if (options.validate) { try { const errors = await options.validate(values); const hasErrors = Object.keys(errors).length > 0; if (hasErrors) { setFormState(prev => ({ ...prev, isSubmitting: false, isValid: false, fields: Object.keys(prev.fields).reduce((acc, key) => ({ ...acc, [key]: { ...prev.fields[key as keyof T], error: errors[key], touched: true, } }), {} as { [K in keyof T]: FormField }), })); return; } } catch (validationError) { logErrorWithContext(validationError, { timestamp: new Date().toISOString(), component: options.component || 'useEnhancedForm', action: 'validate', formData: values, }); setFormState(prev => ({ ...prev, isSubmitting: false, submitError: 'Validation failed. Please check your input.', })); return; } } // Submit the form try { await options.onSubmit(values); setFormState(prev => ({ ...prev, isSubmitting: false, submitError: undefined, })); options.onSuccess?.(values); } catch (error) { const parsed = parseApiError(error); // Log for developers logErrorWithContext(error, { timestamp: new Date().toISOString(), component: options.component || 'useEnhancedForm', action: 'submit', formData: values, }); // Handle validation errors from API if (parsed.isValidationError && parsed.validationErrors.length > 0) { const fieldErrors = formatValidationErrorsForForm(parsed.validationErrors); setFormState(prev => ({ ...prev, isSubmitting: false, isValid: false, fields: Object.keys(prev.fields).reduce((acc, key) => ({ ...acc, [key]: { ...prev.fields[key as keyof T], error: fieldErrors[key], touched: true, } }), {} as { [K in keyof T]: FormField }), })); } else { // General submit error setFormState(prev => ({ ...prev, isSubmitting: false, submitError: parsed.userMessage, })); } options.onError?.(error, values); } }, [getValues, options]); return { formState, setFormState, handleChange, setFieldValue, setFieldError, handleSubmit, reset, setFormError, clearFieldError, validateField, }; }