import { useCallback, useMemo, useState } from 'react'; type FieldErrors = Partial>; type TouchedFields = Partial>; type SetValuesOptions = { validate?: boolean; clearErrors?: boolean; }; type SetFieldValueOptions = { validate?: boolean; touch?: boolean; }; type ValidateAllOptions = { touchAll?: boolean; }; type UseValidatedFieldsOptions = { initialValues: TValues; validate: (values: TValues) => FieldErrors; }; function hasErrors(errors: FieldErrors): boolean { return Object.values(errors).some(Boolean); } function pickTouchedErrors( errors: FieldErrors, touched: TouchedFields, ): FieldErrors { const next: FieldErrors = {}; for (const key of Object.keys(errors) as Array) { if (touched[key]) { next[key] = errors[key]; } } return next; } function touchAll>(values: TValues): TouchedFields { const touched: TouchedFields = {}; for (const key of Object.keys(values) as Array) { touched[key] = true; } return touched; } export function useValidatedFields>({ initialValues, validate, }: UseValidatedFieldsOptions) { const [values, setValues] = useState(initialValues); const [allErrors, setAllErrors] = useState>(() => validate(initialValues)); const [touched, setTouched] = useState>({}); const updateValues = useCallback( (nextValues: TValues, options: SetValuesOptions = {}) => { const { validate: shouldValidate = false, clearErrors = false } = options; setValues(nextValues); if (shouldValidate || clearErrors) { setAllErrors(validate(nextValues)); } if (clearErrors) { setTouched({}); } }, [validate], ); const setFieldValue = useCallback( ( key: K, value: TValues[K], options: SetFieldValueOptions = {}, ) => { const { validate: shouldValidate = true, touch = true } = options; if (touch) { setTouched((current) => ({ ...current, [key]: true, })); } setValues((current) => { const nextValues = { ...current, [key]: value, }; if (shouldValidate) { setAllErrors(validate(nextValues)); } return nextValues; }); }, [validate], ); const validateAll = useCallback( (options: ValidateAllOptions = {}) => { const { touchAll: shouldTouchAll = true } = options; const nextErrors = validate(values); setAllErrors(nextErrors); if (shouldTouchAll) { setTouched(touchAll(values)); } return nextErrors; }, [validate, values], ); const setFieldError = useCallback((key: K, message?: string) => { setTouched((current) => ({ ...current, [key]: true, })); setAllErrors((current) => ({ ...current, [key]: message, })); }, []); const updateErrors = useCallback((nextErrors: FieldErrors) => { const nextTouched: TouchedFields = {}; for (const key of Object.keys(nextErrors) as Array) { if (nextErrors[key]) { nextTouched[key] = true; } } setTouched((current) => ({ ...current, ...nextTouched, })); setAllErrors(nextErrors); }, []); const clearErrors = useCallback(() => { setAllErrors(validate(values)); setTouched({}); }, [validate, values]); const errors = useMemo(() => pickTouchedErrors(allErrors, touched), [allErrors, touched]); const isValid = useMemo(() => { return !hasErrors(validate(values)); }, [validate, values]); return { values, errors, isValid, setValues: updateValues, setFieldValue, validateAll, setFieldError, setErrors: updateErrors, clearErrors, }; }