// hooks/useForm.ts
import { useState, useCallback } from 'react';
interface UseFormOptions<T> {
initialValues: T;
validate?: (values: T) => Partial<Record<keyof T, string>>;
onSubmit: (values: T) => Promise<void> | void;
}
function useForm<T extends Record<string, any>>({
initialValues,
validate,
onSubmit
}: UseFormOptions<T>) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = useCallback((name: keyof T, value: any) => {
setValues(prev => ({ ...prev, [name]: value }));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: undefined }));
}
}, [errors]);
const handleBlur = useCallback((name: keyof T) => {
setTouched(prev => ({ ...prev, [name]: true }));
if (validate) {
const fieldErrors = validate(values);
if (fieldErrors[name]) {
setErrors(prev => ({ ...prev, [name]: fieldErrors[name] }));
}
}
}, [values, validate]);
const handleSubmit = useCallback(async (e?: React.FormEvent) => {
e?.preventDefault();
if (validate) {
const formErrors = validate(values);
setErrors(formErrors);
if (Object.keys(formErrors).length > 0) {
return;
}
}
setIsSubmitting(true);
try {
await onSubmit(values);
} catch (error) {
console.error('Form submission error:', error);
} finally {
setIsSubmitting(false);
}
}, [values, validate, onSubmit]);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
setIsSubmitting(false);
}, [initialValues]);
return {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
reset
};
}
// Usage
interface LoginForm {
email: string;
password: string;
}
function LoginPage() {
const form = useForm<LoginForm>({
initialValues: { email: '', password: '' },
validate: (values) => {
const errors: Partial<Record<keyof LoginForm, string>> = {};
if (!values.email) {
errors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(values.email)) {
errors.email = 'Email is invalid';
}
if (!values.password) {
errors.password = 'Password is required';
} else if (values.password.length < 6) {
errors.password = 'Password must be at least 6 characters';
}
return errors;
},
onSubmit: async (values) => {
await login(values);
}
});
return (
<form onSubmit={form.handleSubmit}>
<input
type="email"
value={form.values.email}
onChange={(e) => form.handleChange('email', e.target.value)}
onBlur={() => form.handleBlur('email')}
/>
{form.touched.email && form.errors.email && (
<span>{form.errors.email}</span>
)}
<input
type="password"
value={form.values.password}
onChange={(e) => form.handleChange('password', e.target.value)}
onBlur={() => form.handleBlur('password')}
/>
{form.touched.password && form.errors.password && (
<span>{form.errors.password}</span>
)}
<button type="submit" disabled={form.isSubmitting}>
{form.isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
);
}