Controlled vs Uncontrolled Inputs
In React 19, you don't need to write fully controlled inputs manually for most native fields. But controlled patterns (especially via Controller) are still recommended when working with modern UI libraries or rich interactive forms.
- React Hook Form embraces uncontrolled components but is fully compatible with controlled components.
- Most UI libraries are built only for controlled mode, so mixing is the standard way to get the best of both worlds:
- Performance from uncontrolled native fields
- Flexibility from controlled UI library components
What's the difference?
Controlled — React state is the single source of truth. Every keystroke updates state and re-renders.
function ControlledInput() {
const [email, setEmail] = useState("");
return (
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
);
}Uncontrolled — The DOM holds the value. You read it on submit (via ref or FormData).
function UncontrolledInput() {
const ref = useRef<HTMLInputElement>(null);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
console.log(ref.current?.value);
}
return (
<form onSubmit={handleSubmit}>
<input type="email" ref={ref} defaultValue="" />
<button type="submit">Submit</button>
</form>
);
}Uncontrolled with React 19 server actions
The modern default for simple forms — no state, no refs, no re-renders.
async function subscribe(formData: FormData) {
"use server";
const email = formData.get("email");
await saveSubscriber(email);
}
export function NewsletterForm() {
return (
<form action={subscribe}>
<input name="email" type="email" required />
<button type="submit">Subscribe</button>
</form>
);
}react-hook-form: uncontrolled by default
register() wires inputs as uncontrolled — fast, minimal re-renders.
import { useForm } from "react-hook-form";
type FormValues = { email: string; password: string };
export function LoginForm() {
const { register, handleSubmit } = useForm<FormValues>();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("email")} type="email" />
<input {...register("password")} type="password" />
<button type="submit">Log in</button>
</form>
);
}react-hook-form: Controller for UI libraries
Use Controller to bridge to controlled components like shadcn/ui Select or MUI.
import { Controller, useForm } from "react-hook-form";
import { Select, SelectTrigger, SelectContent, SelectItem } from "@/components/ui/select";
export function PlanForm() {
const { control, handleSubmit } = useForm({ defaultValues: { plan: "free" } });
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="plan"
control={control}
render={({ field }) => (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger />
<SelectContent>
<SelectItem value="free">Free</SelectItem>
<SelectItem value="pro">Pro</SelectItem>
</SelectContent>
</Select>
)}
/>
<button type="submit">Save</button>
</form>
);
}When to use which
- Most native
<input>,<textarea>,<select>→ uncontrolled withregister()or server actions - Custom or third-party components (shadcn/ui, MUI, etc.) → controlled with
<Controller> - Need real-time logic on some fields, performance on others → mix as needed
Important rules to avoid bugs
- Never switch a single input from controlled to uncontrolled (or vice versa) during its lifetime — this triggers React warnings.
- Do not manually add
value+onChangeto a field that usesregister()— it breaks the uncontrolled pattern. - For the same field name, stick to one approach consistently.
- Always provide
defaultValue(notvalue) for uncontrolled inputs to avoid the controlled/uncontrolled warning.
2026 React 19 / Next.js Decision Guide
Uncontrolled Inputs (Native React 19 approach) – Use this when most of these are true:
- The form is simple (login, contact, newsletter, basic settings — usually under 8 fields)
- You mainly need the values only on submit
- You are using server actions with
<form action={...}> - You want minimal re-renders and the best possible performance
- You plan to use
useActionStateanduseFormStatus - Progressive enhancement matters (form should still work if JavaScript fails)
- You are okay relying mostly on HTML5 validation plus server-side validation
- You want the simplest code with automatic form reset on successful submission
- There are no complex real-time UI updates or interdependent fields
- You prefer less boilerplate and a smaller bundle size
Controlled Inputs or react-hook-form – Use this when most of these are true:
- The form has many fields (over 8–10) or is multi-step
- You need real-time validation, formatting, or live feedback as the user types
- Fields depend on each other (show/hide, enable/disable, or perform calculations)
- You need to pre-populate the form with data from an API
- You want instant client-side error messages with excellent UX
- You have complex client-side logic before submission (password strength, totals, live previews)
- You are using TypeScript and want strong type safety across the form
- You need fine-grained control over UI updates
- The form includes file uploads with preview or progress indicators
- You want to keep most logic on the client before sending data to the server
Quick Decision Guide (2026 Best Practice)
- Simple form + Server Actions → Uncontrolled inputs with
useActionStateanduseFormStatus - Complex, multi-step, or rich UX → react-hook-form + zod (still uses uncontrolled inputs internally for performance)
- Editing existing data → Usually react-hook-form with
reset()or nativerequestFormReset - Maximum performance and minimal code → Native uncontrolled inputs with React 19 hooks
- Heavy real-time interactivity → react-hook-form for the best balance of control and performance
Default Recommendation in 2026
Start with uncontrolled inputs + React 19 native hooks for any new simple or medium-sized form. Switch to react-hook-form the moment you need real-time validation, conditional fields, or complex client-side logic.
Even when using react-hook-form, you still benefit from uncontrolled input performance under the hood.