typescriptintermediate
React Form Validation Hook
A type-safe form validation hook with field-level errors, dirty tracking, and submit handling.
typescriptPress ⌘/Ctrl + Shift + C to copy
'use client';
import { useState, useCallback, FormEvent } from 'react';
type Errors<T> = Partial<Record<keyof T, string>>;
type Validators<T> = Partial<Record<keyof T, (value: T[keyof T], values: T) => string | undefined>>;
function useForm<T extends Record<string, unknown>>({
initialValues,
validators,
onSubmit,
}: {
initialValues: T;
validators?: Validators<T>;
onSubmit: (values: T) => void | Promise<void>;
}) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Errors<T>>({});
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
const [submitting, setSubmitting] = useState(false);
const setValue = useCallback(<K extends keyof T>(field: K, value: T[K]) => {
setValues((prev) => ({ ...prev, [field]: value }));
if (validators?.[field]) {
const error = validators[field]!(value, { ...values, [field]: value });
setErrors((prev) => ({ ...prev, [field]: error }));
}
}, [validators, values]);
const setFieldTouched = useCallback((field: keyof T) => {
setTouched((prev) => ({ ...prev, [field]: true }));
}, []);
const validate = useCallback((): boolean => {
if (!validators) return true;
const newErrors: Errors<T> = {};
let valid = true;
for (const [field, validator] of Object.entries(validators)) {
const error = (validator as Function)(values[field as keyof T], values);
if (error) {
newErrors[field as keyof T] = error;
valid = false;
}
}
setErrors(newErrors);
return valid;
}, [validators, values]);
const handleSubmit = useCallback(async (e: FormEvent) => {
e.preventDefault();
if (!validate()) return;
setSubmitting(true);
try {
await onSubmit(values);
} finally {
setSubmitting(false);
}
}, [validate, onSubmit, values]);
return { values, errors, touched, submitting, setValue, setFieldTouched, handleSubmit };
}
// Usage
function SignupForm() {
const form = useForm({
initialValues: { email: '', password: '' },
validators: {
email: (v) => (!String(v).includes('@') ? 'Invalid email' : undefined),
password: (v) => (String(v).length < 8 ? 'Min 8 chars' : undefined),
},
onSubmit: async (vals) => console.log('Submit:', vals),
});
return (
<form onSubmit={form.handleSubmit}>
<input value={String(form.values.email)}
onChange={(e) => form.setValue('email', e.target.value)}
onBlur={() => form.setFieldTouched('email')} />
{form.touched.email && form.errors.email && <span>{form.errors.email}</span>}
<button type="submit" disabled={form.submitting}>Submit</button>
</form>
);
}Use Cases
- Type-safe form handling without libraries
- Field-level validation with error display
- Reusable form logic across components
Tags
Related Snippets
Similar patterns you can reuse in the same workflow.
typescriptintermediate
useFormValidation Hook
Lightweight form validation hook with field-level errors, touched tracking, and submit handling.
Best for: Contact forms
#hooks#forms
typescriptbeginner
usePrevious Hook
Track the previous value of any state or prop using a ref-based hook for comparison logic.
Best for: Detecting value changes
#hooks#state
typescriptintermediate
React Custom Hook for Data Fetching
A reusable useFetch hook with loading states, error handling, caching, and abort support.
Best for: Reusable data fetching across components
#react#hooks
typescriptintermediate
React Intersection Observer Hook
Custom useIntersectionObserver hook for lazy loading, infinite scroll, and scroll animations.
Best for: Lazy loading components when they enter viewport
#react#hooks