React
Forms in React: React Hook Form Patterns You'll Actually Reuse
Stable patterns for validation, error display, and controlled vs uncontrolled fields with React Hook Form.

React Hook Form keeps forms fast by minimizing re-renders and leaning on native validation where possible. These patterns will carry across projects without turning into a mess.
Register and uncontrolled by default
Register inputs with register("fieldName") and let the DOM hold the value. That avoids a state update on every keystroke. Use uncontrolled fields for simple text inputs, checkboxes, and selects when you don’t need to drive the value from outside.
import { useForm } from "react-hook-form";
function SignupForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input
{...register("email", {
required: "Email is required",
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: "Invalid email",
},
})}
/>
{errors.email && <span role="alert">{errors.email.message}</span>}
<button type="submit">Submit</button>
</form>
);
}Keep validation in the second argument to register for small rules; move to zod or yup with @hookform/resolvers when you need shared schemas or complex rules.
When to use control (controlled mode)
Use control and Controller when the input isn’t a native form element (e.g. a custom design-system input, a rich text editor, or a third-party date picker). Same when you need to set the value programmatically or sync with external state.
<Controller
name="startDate"
control={control}
rules={{ required: true }}
render={({ field }) => (
<DatePicker
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
ref={field.ref}
/>
)}
/>For native inputs, stick with register unless you have a clear reason to go controlled.
Error display and accessibility
Show errors near the field and associate them with aria-describedby or aria-invalid so screen readers announce them. Use formState.errors and render a single message per field. Avoid clearing errors on every keystroke if you want to show them only after submit or first blur; React Hook Form’s mode option controls that (e.g. onSubmit, onTouched).
Reusable field wrapper
Wrap label, input, and error in a small component that accepts label, error, and spreads ...register(...) or a Controller’s field. That keeps forms DRY and ensures consistent structure and accessibility. Use the same wrapper for select, checkbox, and textarea; only the inner element changes.
Summary
Use register and uncontrolled inputs by default; use Controller for custom or third-party components. Put validation in register or a resolver, show errors accessibly, and reuse a field wrapper so forms stay consistent and maintainable.