Skip to content

React Forms

React has two approaches to form inputs: controlled (React drives the value) and uncontrolled (the DOM drives the value). For most forms, use React Hook Form.

The input value is bound to React state:

function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
);
}

Works fine for small forms. Becomes tedious with many fields — use React Hook Form for anything larger.

Minimal re-renders, built-in validation, great TypeScript support:

Terminal window
npm install react-hook-form
import { useForm, SubmitHandler } from 'react-hook-form';
interface LoginInputs {
email: string;
password: string;
}
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<LoginInputs>();
const onSubmit: SubmitHandler<LoginInputs> = async (data) => {
await login(data.email, data.password);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="email"
{...register('email', {
required: 'Email is required',
pattern: { value: /^\S+@\S+$/i, message: 'Invalid email' },
})}
/>
{errors.email && <span>{errors.email.message}</span>}
<input
type="password"
{...register('password', {
required: 'Password is required',
minLength: { value: 8, message: 'Min 8 characters' },
})}
/>
{errors.password && <span>{errors.password.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
);
}

Use Zod for schema-based validation (integrates with React Hook Form):

Terminal window
npm install zod @hookform/resolvers
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Min 8 characters'),
age: z.number().min(18, 'Must be 18+'),
});
type FormData = z.infer<typeof schema>;
function RegisterForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input type="email" {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
<input type="number" {...register('age', { valueAsNumber: true })} />
{errors.age && <span>{errors.age.message}</span>}
<button type="submit">Register</button>
</form>
);
}
const { register, watch } = useForm();
const role = watch('role');
return (
<form>
{/* Select */}
<select {...register('role')}>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
{/* Checkbox */}
<input type="checkbox" {...register('acceptTerms')} />
{/* Radio */}
<input type="radio" value="male" {...register('gender')} />
<input type="radio" value="female" {...register('gender')} />
</form>
);
import { useFieldArray } from 'react-hook-form';
function TagsForm() {
const { control, register } = useForm({ defaultValues: { tags: [{ name: '' }] } });
const { fields, append, remove } = useFieldArray({ control, name: 'tags' });
return (
<form>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`tags.${index}.name`)} />
<button type="button" onClick={() => remove(index)}>Remove</button>
</div>
))}
<button type="button" onClick={() => append({ name: '' })}>Add Tag</button>
</form>
);
}
function FileUpload() {
const { register, handleSubmit } = useForm();
const onSubmit = (data: { file: FileList }) => {
const formData = new FormData();
formData.append('file', data.file[0]);
fetch('/api/upload', { method: 'POST', body: formData });
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="file" {...register('file')} />
<button type="submit">Upload</button>
</form>
);
}