Quiz

What is the difference between controlled and uncontrolled React Components?

Topics
React

TL;DR

A controlled component drives a form input from React state — you pass value/checked plus an onChange handler, and React state is the single source of truth. An uncontrolled component lets the DOM keep the value; you read it via a ref (or on submit) and seed the initial value with defaultValue/defaultChecked. Controlled inputs are the right default when you need validation, conditional UI, or to derive other state from the value. Uncontrolled inputs are simpler for write-once forms and for <input type="file">, which is always uncontrolled. React 19 also added first-class form support via the form action prop, useFormStatus, and useActionState, which often removes the need for per-field controlled state.


What is the difference between controlled and uncontrolled React components?

Controlled components

A controlled input passes both value (or checked) and onChange to the element. React state holds the truth; every keystroke flows through a setter.

import { useState } from 'react';
function ControlledForm() {
const [name, setName] = useState('');
function handleSubmit(event) {
event.preventDefault();
alert('A name was submitted: ' + name);
}
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input
type="text"
value={name}
onChange={(event) => setName(event.target.value)}
/>
</label>
<input type="submit" value="Submit" />
</form>
);
}

Uncontrolled components

An uncontrolled input keeps its value in the DOM. Seed the initial value with defaultValue (or defaultChecked for checkboxes/radios), and read the current value through a ref when you need it.

import { useRef } from 'react';
function UncontrolledForm() {
const inputRef = useRef(null);
function handleSubmit(event) {
event.preventDefault();
alert('A name was submitted: ' + inputRef.current.value);
}
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" defaultValue="" ref={inputRef} />
</label>
<input type="submit" value="Submit" />
</form>
);
}

defaultValue is only consulted on the initial render — changing it later does not update the DOM. Pairing value with no onChange (or vice versa) makes the input read-only or warns in development; pick one mode per field.

<input type="file"> is always uncontrolled

File inputs cannot be controlled — their value is read-only for security reasons (a page must not be able to set the user's chosen file). Always read files via a ref or from the change/submit event, even in an otherwise controlled form.

function FileForm() {
const fileRef = useRef(null);
function handleSubmit(event) {
event.preventDefault();
const file = fileRef.current.files[0];
// upload file...
}
return (
<form onSubmit={handleSubmit}>
<input type="file" ref={fileRef} />
<button type="submit">Upload</button>
</form>
);
}

React 19 form actions

React 19 made <form action={...}> a first-class way to handle submissions without per-field controlled state. The action receives a FormData object, and useFormStatus / useActionState expose pending and result state.

import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? 'Saving...' : 'Save'}</button>;
}
async function saveName(prevState, formData) {
const name = formData.get('name');
// ...persist...
return { ok: true, name };
}
function NameForm() {
const [state, formAction] = useActionState(saveName, null);
return (
<form action={formAction}>
<input name="name" defaultValue={state?.name ?? ''} />
<SubmitButton />
</form>
);
}

This pattern uses uncontrolled inputs (defaultValue plus name) and reads them out of FormData in the action — often the simplest choice for plain submit-style forms.

Key differences

State management

  • Controlled: React state owns the value; the DOM mirrors it.
  • Uncontrolled: The DOM owns the value; React reads it on demand.

Data flow

  • Controlled: state -> value -> input, and onChange -> setState.
  • Uncontrolled: defaultValue -> input, then ref.current.value (or FormData) when read.

When to use which

  • Controlled: validation as the user types, conditional disabling, formatting, deriving other state, or anything that needs to react to every keystroke.
  • Uncontrolled: simple submit-once forms, integration with non-React code, file inputs, and forms built around React 19 actions.

Further reading

Edit on GitHub