patterntypescriptreactTip
React Hook Form — setup with TypeScript and controlled inputs
Viewed 0 times
react hook formuseFormregisterhandleSubmitSubmitHandlerformStateuncontrolled form
Problem
Managing form state with useState creates one state variable per field, manual change handlers, and complex validation logic scattered across the component. React Hook Form centralises this with a performant, uncontrolled-by-default approach.
Solution
Use useForm with a TypeScript interface; register fields and handle submission:
import { useForm, SubmitHandler } from 'react-hook-form';
interface LoginForm {
email: string;
password: string;
rememberMe: boolean;
}
export function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
} = useForm<LoginForm>({
defaultValues: { email: '', password: '', rememberMe: false },
});
const onSubmit: SubmitHandler<LoginForm> = async (data) => {
await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(data),
});
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('email', {
required: 'Email is required',
pattern: { value: /^[^@]+@[^@]+$/, message: 'Invalid email' },
})}
type="email"
/>
{errors.email && <span>{errors.email.message}</span>}
<input
{...register('password', { required: 'Password is required', minLength: 8 })}
type="password"
/>
{errors.password && <span>{errors.password.message}</span>}
<input {...register('rememberMe')} type="checkbox" />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
);
}
import { useForm, SubmitHandler } from 'react-hook-form';
interface LoginForm {
email: string;
password: string;
rememberMe: boolean;
}
export function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
} = useForm<LoginForm>({
defaultValues: { email: '', password: '', rememberMe: false },
});
const onSubmit: SubmitHandler<LoginForm> = async (data) => {
await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(data),
});
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('email', {
required: 'Email is required',
pattern: { value: /^[^@]+@[^@]+$/, message: 'Invalid email' },
})}
type="email"
/>
{errors.email && <span>{errors.email.message}</span>}
<input
{...register('password', { required: 'Password is required', minLength: 8 })}
type="password"
/>
{errors.password && <span>{errors.password.message}</span>}
<input {...register('rememberMe')} type="checkbox" />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
);
}
Why
React Hook Form uses uncontrolled inputs by default — the DOM holds the value, not React state. This means only the affected input re-renders on change, not the entire form, making large forms much faster than useState-based alternatives.
Gotchas
- register returns { name, ref, onChange, onBlur } — spread all of them onto the input, not just ref
- For controlled components (e.g. custom UI libraries), use Controller instead of register
- handleSubmit only calls onSubmit when validation passes — it silently prevents submission on errors
- reset() without arguments restores defaultValues — call it after successful submission to clear the form
Revisions (0)
No revisions yet.