Value ownership is the difference

An input’s value changes as someone types, selects an option, or checks a box. The question is where the component treats the authoritative value as living. In a controlled input, component state owns it: the component supplies the displayed value and handles changes to that state. In an uncontrolled input, the browser’s input element owns its current value, and the component reads it when needed.

The examples below assume a browser app using React function components and hooks. They use standard text inputs unless noted otherwise. Both approaches can produce a usable form; the choice depends on when the rest of the interface needs to know about a change.

Controlled inputs: state drives the field

For a controlled text field, initialize state with const [name, setName] = useState(''). Render an input with value={name} and onChange={event => setName(event.target.value)}. When the person types, the change handler updates state, React renders again, and the new state value appears in the field. Keep the value and the handler together: a value prop without a working update path can leave the field effectively read-only.

This pattern helps when another part of the interface must respond while the person edits. A character count can use name.length, a preview can display name, and a button can be disabled when name.trim() is empty. Since state is the source of truth, setting the state back to '' also clears the field. If a text field starts empty, initialize its state to an empty string rather than leaving its value undefined and assigning a string later.

Controlled does not mean every keystroke needs complex validation. You can store the raw text while typing and show an error only after a blur event or a submit attempt. That avoids interrupting someone who has not finished entering a value.

Uncontrolled inputs: read from the element

For an uncontrolled text field, omit the value prop. The browser maintains what the person typed. In a React form, you can give the field name="displayName" and read it on submission: prevent the default submit, create new FormData(event.currentTarget), then call data.get('displayName'). This is a useful choice when you only need the final answer, not an update on every edit.

Use defaultValue="Guest" to provide an initial text value without controlling subsequent edits. Changing that defaultValue prop later is not a reliable way to replace what a person has typed; it describes the initial value, not an ongoing command. For a checkbox, the corresponding starting prop is defaultChecked, while a controlled checkbox uses checked and reads event.target.checked in its change handler.

A ref offers another way to read or focus a particular uncontrolled input, but FormData is often simpler for a whole form. Give each field a name if you expect FormData to include it. To clear uncontrolled fields after a successful submission, call event.currentTarget.reset() while the form element is still available. Test the reset behavior if your form also contains controlled fields, because their displayed values remain tied to component state.

Choose based on what the interface needs

Ask when another part of the component needs the value. If a live preview, dependent field, or immediate feedback must reflect edits, controlled state is usually straightforward. If the value matters mainly at submission, an uncontrolled field can avoid state and change-handler wiring. Neither approach removes the need to check submitted values: input constraints and client-side feedback improve the experience but do not make an untrusted value safe to use elsewhere.

Be deliberate when mixing approaches. A form can contain both, but each field should have a clear owner. Do not put both value and defaultValue on the same text input, and do not assume that changing state will update a field you left uncontrolled. File inputs are a special case: the browser controls their selected files. Read the chosen files from the input or submitted form rather than trying to set a file input’s value from React state.

For either approach, connect each visible label to its input with matching htmlFor and id values. If you display an error, place it near the field and associate it with the input, for example through aria-describedby. Value ownership determines how data moves; it does not replace clear labels, helpful instructions, or a predictable keyboard experience.

A small test for the right choice

Build the same one-field form both ways. In the controlled version, show a live count and clear the field by updating state. In the uncontrolled version, read the named field with FormData on submit and clear it with the form’s reset method. Type into each, submit, and observe when the component knows the current value.

Then add a requirement: show a preview after every change. The controlled version already has the value available for rendering. If that preview is unnecessary, the uncontrolled version may be the smaller solution. The useful distinction is not which pattern is more advanced; it is whether the component or the browser should own the value between interactions.

Back to category ↗