Uncontrolled inputs
Lets the DOM own a field's value, seeding it with defaultValue and reading it through a ref.
const inputRef = useRef(null);
<input defaultValue="initial" ref={inputRef} />
// later: inputRef.current.valueParameters
| Parameter | Type | Description |
|---|---|---|
$defaultValueoptional | string | The field's initial value. Used by <input>, <textarea> and <select>; changing it after mount has no effect on what the user sees. |
$defaultCheckedoptional | boolean | The initial state for <input type="checkbox"> and type="radio", used in place of defaultValue. |
$refoptional | RefObject<HTMLInputElement> | ((node: HTMLInputElement | null) => void) | Gives access to the DOM node so its current value can be read with ref.current.value. Since React 19, ref is an ordinary prop, so a wrapper function component can forward it without forwardRef. |
$nameoptional | string | Required if the field should appear in the form's FormData, which is the alternative to refs for reading many fields at once. |
Return value
Not a callable API. The DOM node holds the current value; React only sets the initial one. Read it on demand from ref.current.value, or from the form's FormData.
Errors
React logs an error if a field is given both value and defaultValue, and if it later switches from uncontrolled to controlled by receiving a defined value prop.
Changelog
- PHP 19.0.0ref is a normal prop on function components, so a wrapper around an uncontrolled input no longer needs forwardRef.
- PHP 19.0.0React resets uncontrolled fields to their defaultValue after a <form> function action completes successfully.
An uncontrolled field keeps its value in the DOM. React sets the starting text through defaultValue — or defaultChecked for checkboxes and radios — and then stops paying attention. Nothing re-renders while the user types, and you read the result when you actually need it, either from ref.current.value or from the form's FormData.
When to reach for it
Use it for the ordinary case: a form whose fields you read once, on submit. It is less code than a controlled field, it never causes a re-render per keystroke, and it keeps a large form responsive. It is also the only option for <input type="file">, whose value scripts cannot set.
What trips people up
defaultValueis read once, at mount. Changing it later does nothing — a field seeded from data that arrives asynchronously will keep showing the empty original unless you remount it with akey.- Reading is a snapshot.
ref.current.valuetells you the text now; it does not make the component re-render when the text changes. - A field with no
namenever appears inFormData, however carefully you built the form. - Passing both
valueanddefaultValueis an error — pick one owner.
Example
When to use it
- Read a signup form's fields once when the user submits
- Keep a long form responsive by not re-rendering on every keystroke
- Accept a file upload, which can never be a controlled field
Frequently asked questions
What is the difference between defaultValue and value in React?
value makes the field controlled: React forces the DOM to match it on every render, so an onChange handler is required. defaultValue only sets the initial text and is ignored afterwards, leaving the DOM in charge. Passing both to the same field is an error.
Why doesn't my input update when defaultValue changes?
defaultValue is read once, when the DOM node is created, so later changes are ignored by design. Either make the field controlled with value and onChange, or force a remount by giving the field a key that changes when the underlying record does.
How do I read an input's value without putting it in state?
Attach a ref and read ref.current.value when you need it — typically in a submit handler. For a whole form at once, skip refs entirely and build a FormData from the form element: new FormData(event.currentTarget), then data.get('fieldName'). Only fields with a name prop are included.
More examples
Reading the whole form without any refs
Submitting the untouched form renders `item=Espresso | notes=Extra hot | quantity=2 | size=m | togo=on`, sorted so the output is stable. Five different field types are read with no refs and no state for the fields themselves — FormData collects anything with a name prop. Each type takes its initial value the same way: defaultValue everywhere except the checkbox, which takes defaultChecked, and the select, where defaultValue lives on the parent rather than on an option.
import { useState } from "react";
export default function OrderForm() {
const [summary, setSummary] = useState("nothing submitted");
function handleSubmit(e) {
e.preventDefault();
const data = new FormData(e.currentTarget);
setSummary(
[...data.entries()].map(([k, v]) => `${k}=${v}`).sort().join(" | ")
);
}
return (
<form onSubmit={handleSubmit}>
<input name="item" defaultValue="Espresso" />
<input name="quantity" type="number" defaultValue={2} />
<select name="size" defaultValue="m">
<option value="s">Small</option>
<option value="m">Medium</option>
</select>
<textarea name="notes" defaultValue="Extra hot" rows={2} />
<label>
<input name="togo" type="checkbox" defaultChecked /> To go
</label>
<button type="submit">Order</button>
<p>{summary}</p>
</form>
);
}Reseeding a field when its data arrives
The field renders with "Grace Hopper" already in it. Without the key it would render empty forever: defaultValue is only consulted when the DOM node is created, and a later prop change is ignored by design. Giving the field a key tied to the record's id makes React discard the old node and build a new one whenever a different profile loads, which also discards any half-typed edits — the intended behaviour when switching records.
import { useEffect, useRef, useState } from "react";
function NameField({ initialName }) {
const ref = useRef(null);
return <input ref={ref} name="name" defaultValue={initialName} />;
}
export default function EditProfile() {
const [profile, setProfile] = useState(null);
useEffect(() => {
// No network here; a resolved promise stands in for the load.
Promise.resolve({ id: 1, name: "Grace Hopper" }).then(setProfile);
}, []);
if (!profile) return <p>Loading...</p>;
// The key forces a fresh DOM node, so defaultValue applies again.
return (
<form>
<NameField key={profile.id} initialName={profile.name} />
</form>
);
}Focusing and clearing through the ref
Search reads the DOM node once and copies the text into state, so `Last search: react forms` appears only after the click, not while typing. Clear writes the empty string straight to the node and moves focus there. Assigning to ref.current.value is legitimate precisely because this field is uncontrolled — on a controlled field the same line would be undone by the next render, since the value prop would win.
import { useRef, useState } from "react";
export default function SearchBox() {
const inputRef = useRef(null);
const [lastSearch, setLastSearch] = useState("");
function search() {
setLastSearch(inputRef.current.value);
}
function clear() {
inputRef.current.value = "";
inputRef.current.focus();
setLastSearch("");
}
return (
<div>
<input ref={inputRef} defaultValue="react forms" />
<button onClick={search}>Search</button>
<button onClick={clear}>Clear</button>
<p>Last search: {lastSearch === "" ? "(none)" : lastSearch}</p>
</div>
);
}
Discussion