import { useState, useCallback, useEffect } from 'react'; import { z } from 'zod'; interface UseFormValidationOptions { schema: z.ZodSchema; initialValues: Partial; validateOnChange?: boolean; validateOnBlur?: boolean; } interface FormState { values: Partial; errors: Record; touched: Record; isValid: boolean; isSubmitting: boolean; } export function useFormValidation>({ schema, initialValues, validateOnChange = true, validateOnBlur = true, }: UseFormValidationOptions) { const [state, setState] = useState>({ values: initialValues, errors: {}, touched: {}, isValid: false, isSubmitting: false, }); // Validate a single field const validateField = useCallback((name: keyof T, value: any): string | null => { try { // Get the field schema const fieldSchema = (schema as any).shape[name]; if (fieldSchema) { fieldSchema.parse(value); } return null; } catch (error) { if (error instanceof z.ZodError) { return error.errors[0]?.message || null; } return null; } }, [schema]); // Validate all fields const validateForm = useCallback((values: Partial): Record => { try { schema.parse(values); return {}; } catch (error) { if (error instanceof z.ZodError) { const errors: Record = {}; error.errors.forEach((err) => { if (err.path.length > 0) { errors[err.path[0] as string] = err.message; } }); return errors; } return {}; } }, [schema]); // Set field value const setValue = useCallback((name: keyof T, value: any) => { setState(prev => { const newValues = { ...prev.values, [name]: value }; const fieldError = validateOnChange ? validateField(name, value) : null; const newErrors = { ...prev.errors }; if (fieldError) { newErrors[name as string] = fieldError; } else { delete newErrors[name as string]; } const allErrors = validateOnChange ? validateForm(newValues) : newErrors; const isValid = Object.keys(allErrors).length === 0; return { ...prev, values: newValues, errors: allErrors, isValid, }; }); }, [validateField, validateForm, validateOnChange]); // Set field as touched const setTouched = useCallback((name: keyof T, touched = true) => { setState(prev => { const newTouched = { ...prev.touched, [name]: touched }; let newErrors = { ...prev.errors }; if (touched && validateOnBlur) { const fieldError = validateField(name, prev.values[name]); if (fieldError) { newErrors[name as string] = fieldError; } } return { ...prev, touched: newTouched, errors: newErrors, }; }); }, [validateField, validateOnBlur]); // Set multiple values const setValues = useCallback((values: Partial) => { setState(prev => { const newValues = { ...prev.values, ...values }; const errors = validateForm(newValues); const isValid = Object.keys(errors).length === 0; return { ...prev, values: newValues, errors, isValid, }; }); }, [validateForm]); // Reset form const reset = useCallback((newInitialValues?: Partial) => { const resetValues = newInitialValues || initialValues; setState({ values: resetValues, errors: {}, touched: {}, isValid: false, isSubmitting: false, }); }, [initialValues]); // Set submitting state const setSubmitting = useCallback((isSubmitting: boolean) => { setState(prev => ({ ...prev, isSubmitting })); }, []); // Validate entire form and return validation result const validate = useCallback(() => { const errors = validateForm(state.values); const isValid = Object.keys(errors).length === 0; setState(prev => ({ ...prev, errors, isValid, touched: Object.keys(prev.values).reduce((acc, key) => { acc[key] = true; return acc; }, {} as Record), })); return { isValid, errors }; }, [state.values, validateForm]); // Get field props for easy integration with form components const getFieldProps = useCallback((name: keyof T) => { return { name: name as string, value: state.values[name] || '', error: state.touched[name as string] ? state.errors[name as string] : undefined, onChange: (e: React.ChangeEvent) => { setValue(name, e.target.value); }, onBlur: () => { setTouched(name, true); }, }; }, [state.values, state.errors, state.touched, setValue, setTouched]); // Get field error const getFieldError = useCallback((name: keyof T): string | undefined => { return state.touched[name as string] ? state.errors[name as string] : undefined; }, [state.errors, state.touched]); // Check if field has error const hasFieldError = useCallback((name: keyof T): boolean => { return !!(state.touched[name as string] && state.errors[name as string]); }, [state.errors, state.touched]); // Update validation when schema or initial values change useEffect(() => { const errors = validateForm(state.values); const isValid = Object.keys(errors).length === 0; setState(prev => ({ ...prev, errors, isValid, })); }, [schema, validateForm]); return { values: state.values, errors: state.errors, touched: state.touched, isValid: state.isValid, isSubmitting: state.isSubmitting, setValue, setValues, setTouched, setSubmitting, reset, validate, getFieldProps, getFieldError, hasFieldError, }; }