In one minute
Inputs work in React as they do in HTML, with one big difference: the value prop. An input with value is controlled: React decides what it shows, so typing does nothing unless an onChange updates that value. An input with defaultValue (or defaultChecked) is uncontrolled: the prop is only the starting value, and the browser owns it afterwards. The other surprises are browser rules about what each input puts into FormData.
You’ll be able to: choose between value and defaultValue, and predict exactly what a form submits.
1<input id="pinned" value="Ceramic Mug" />23<input id="prefilled" defaultValue="Ceramic Mug" />- eventThe user types in both. The browser updates each input’s text right away.
- render phase
<input id="pinned" value="Ceramic Mug" />For the first input, React puts its value back: thevalueprop still says"Ceramic Mug", and nothing changed it. <input id="prefilled" defaultValue="Ceramic Mug" />ThedefaultValueinput used the prop only once, when it mounted. The typing stays.<input id="pinned" value="Ceramic Mug" />React also warned that avaluewithout anonChangemakes a read-only field. Fix: addonChangewith state, or usedefaultValue.
value means “React decides what this shows”; defaultValue means “start here, then it’s the user’s” (recorded).The example: value vs defaultValue
r.render(
<>
<input id="pinned" value="Ceramic Mug" />
<input id="prefilled" defaultValue="Ceramic Mug" />
</>,
)
The recorder typed XYZ into the first input and XL into the second:
{
"pinned": "Ceramic Mug",
"prefilled": "Ceramic Mug XL"
}
React also printed this warning, because a value without onChange can never change:
You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.
To make a controlled input editable, store the value in state and update it in onChange (the Hooks module does this). For “pre-fill but let the user edit”, use defaultValue.
The example: a product review form
function submitReview(formData: FormData) {
log('FormData:', Object.fromEntries(formData))
}
r.render(
<form action={submitReview}>
<input type="hidden" name="productId" value="p1" />
<label htmlFor="size">Size bought</label>
<select id="size" name="size">
<option value="">Please choose</option>
<option value="small">Small</option>
<option value="large">Large</option>
</select>
<fieldset>
<legend>Rating</legend>
<label><input type="radio" name="rating" value="5" /> Great</label>
<label><input type="radio" name="rating" value="3" /> Okay</label>
</fieldset>
<label><input type="checkbox" name="recommend" /> I recommend it</label>
<label htmlFor="title">Title</label>
<input id="title" name="title" defaultValue="Love it" />
<button type="submit">Post review</button>
</form>,
)
Submitted without touching anything:
FormData: {"productId":"p1","size":"","title":"Love it"}
Submitted after choosing Large, “Great” and ticking “I recommend it”:
FormData: {"productId":"p1","size":"large","rating":"5","recommend":"on","title":"Love it"}
What ends up in FormData
| Input | Not touched | Chosen / checked |
|---|---|---|
<input type="hidden"> |
always sent (productId: "p1") |
— |
<select> |
the first option’s value (size: "") |
the chosen value |
| radio group | missing | the checked radio’s value |
| checkbox | missing (never false) |
"on", or its value if set |
text with defaultValue |
the default (title: "Love it") |
what the user typed |
| file | the file, if one was picked | — |
So read a checkbox with formData.has('recommend'), and put a “Please choose” option first in a <select> unless a real value should be pre-selected.
How it works
- Group radios with the same
name, and label the group with<fieldset>+<legend>. - Hidden inputs send values the user doesn’t need to see (an id, a context).
- Everything is a string in
FormData:defaultValue={18}and"18"are the same; a date’s value isYYYY-MM-DD. - File inputs can’t have a
valueordefaultValue(browsers forbid pre-selecting a file). To clear one, reset the form, setref.current.value = '', or change itskey(Rendering Arrays).
Common mistakes
valuewithoutonChange→ a read-only field and a warning.- Switching an input between
value={undefined}and a string (uncontrolled → controlled); React warns. - Expecting an unchecked checkbox to send
false. - A select whose first option is a real value, silently submitted by people who never opened it.
Interview Q&A
What's the difference between value and defaultValue?
value makes the input controlled: React sets it on every render, so the user can’t change it unless onChange updates the state behind it. Recorded: typing into <input value="Ceramic Mug" /> left it at "Ceramic Mug". defaultValue only sets the first value; the other input became "Ceramic Mug XL".
How do an unchecked checkbox and an empty radio group appear in FormData?
They don’t: their names are missing. The untouched review form submitted { productId, size: "", title } with no rating and no recommend. A checked checkbox sends "on" unless it has a value.
Why does a <select> always submit something, but a radio group doesn't?
A select always has a selected option (the first one by default), so it always contributes a value (size: "" above, from the placeholder option). Radios have no forced default.
Why can't you set value on a file input?
Browsers block it for security: a page must never choose a file for the user. Reset it with form.reset(), ref.current.value = '', or by remounting it with a new key.
Related
- Forms: how the form is submitted.
- Rendering Arrays: resetting an input with
key.
Sources
- react.dev:
<input>,<select>, Controlled and uncontrolled components - MDN:
FormData,<input type="checkbox"> - Topic order inspired by Kent C. Dodds’ EpicReact React Fundamentals workshop; the example app and code here are this handbook’s own.
Primary source: react.dev/reference/react-dom/components/input