PatternFormValidationInput
Form Validation Pattern
A composable form validation pattern with custom validators, real-time feedback, and TypeScript support.
Preview
Code
import { useState, useCallback } from 'react'
type Validator<T> = (value: T) => string | null
interface FieldState<T> {
value: T
error: string | null
touched: boolean
}
interface FormField<T> {
state: FieldState<T>
onChange: (value: T) => void
onBlur: () => void
validate: () => boolean
reset: () => void
}
export function useFormField<T>(
initialValue: T,
validators: Validator<T>[] = []
): FormField<T> {
const [state, setState] = useState<FieldState<T>>({
value: initialValue,
error: null,
touched: false,
})
const runValidation = useCallback(
(value: T): string | null => {
for (const validator of validators) {
const error = validator(value)
if (error) return error
}
return null
},
[validators]
)
const onChange = useCallback(
(value: T) => {
const error = state.touched ? runValidation(value) : null
setState({ value, error, touched: state.touched })
},
[runValidation, state.touched]
)
const onBlur = useCallback(() => {
setState((prev) => ({
...prev,
touched: true,
error: runValidation(prev.value),
}))
}, [runValidation])
const validate = useCallback(() => {
const error = runValidation(state.value)
setState((prev) => ({ ...prev, touched: true, error }))
return error === null
}, [runValidation, state.value])
const reset = useCallback(() => {
setState({ value: initialValue, error: null, touched: false })
}, [initialValue])
return { state, onChange, onBlur, validate, reset }
}
// Validators
export const required = (msg = 'Required'): Validator<string> =>
(value) => (value.trim() ? null : msg)
export const minLength = (min: number, msg?: string): Validator<string> =>
(value) => (value.length >= min ? null : msg || `Min ${min} characters`)
export const email = (msg = 'Invalid email'): Validator<string> =>
(value) => (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : msg)
// Usage example:
function SignupForm() {
const emailField = useFormField('', [required(), email()])
const passwordField = useFormField('', [required(), minLength(8)])
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (emailField.validate() && passwordField.validate()) {
console.log('Submit:', emailField.state.value, passwordField.state.value)
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={emailField.state.value}
onChange={(e) => emailField.onChange(e.target.value)}
onBlur={emailField.onBlur}
/>
{emailField.state.error && <span>{emailField.state.error}</span>}
{/* ... */}
</form>
)
}Looking for more?
Browse the full collection of patterns or check out other exploration topics.