Controlled vs Uncontrolled Components
Reviewed & published by Brayan K
A controlled input has its value driven by React state (value + onChange) — React is the source of truth. An uncontrolled input keeps its value in the DOM, and you read it on demand with a ref (defaultValue). By the end you'll know which to reach for and how to avoid the dreaded "switching between controlled and uncontrolled" warning.
Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Define controlled and uncontrolled inputs
- Build a controlled input with value + onChange
- Read an uncontrolled input with a ref + defaultValue
- Transform or validate input as the user types
- Know when each approach fits best
- Avoid the controlled/uncontrolled switch warning
1️⃣ Controlled Inputs
In a controlled input you bind value to state and update that state in onChange. The input can only show what state holds, which gives you total control: validate, format, or transform every keystroke before it appears.
function NameField() {
const [name, setName] = useState("");
return (
<input
value={name} // state drives the value
onChange={e => setName(e.target.value.trimStart())} // transform as you type
/>
);
}The demo models a controlled handler that transforms input (here, to uppercase) before storing it in state:
// CONTROLLED input: React state is the source of truth. Every keystroke
// updates state, and the input always shows whatever state holds.
// We model the state + the onChange handler as plain JS.
let state = ""; // this is useState's value
function onChange(typed) {
// React would call setState here; we update our model the same way.
state = typed.toUpperCase(); // you can transform as you type!
}
// Simulate the user typing "react":
onChange("r"); console.log(state); // R
onChange("re"); console.log(state); // RE
onChange("rea"); console.log(state); // REA
onChange("react"); console.log(state);// REACT
// Because state drives the value, you control EXACTLY what appears.Your turn — write a controlled handler that keeps only digits (a numeric field):
// 🎯 YOUR TURN — fill in each ___ then run it.
// Make a controlled handler that keeps ONLY digits (a numeric field).
let value = "";
function onChange(typed) {
// 1) Strip out everything that isn't a digit, then store it in state.
// Hint: typed.replace(/[^0-9]/g, "")
value = ___; // 👉 typed.replace(/[^0-9]/g, "")
}
onChange("a1b2c3");
console.log(value); // should keep only the digits
// ✅ Expected output:
// 1232️⃣ Uncontrolled Inputs
An uncontrolled input lets the DOM keep its own value. You don't track each keystroke in state; instead you grab the value when you need it using a ref. Set the starting value with defaultValue (not value). This is simpler and avoids a re-render per keystroke.
function SearchForm() {
const inputRef = useRef(null);
function handleSubmit(e) {
e.preventDefault();
console.log(inputRef.current.value); // read the DOM value on demand
}
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} defaultValue="" />
<button>Search</button>
</form>
);
}The demo models the DOM node holding its own value, read only at submit time:
// UNCONTROLLED input: the value lives in the DOM. React doesn't track each
// keystroke; you READ it only when you need it (e.g. on submit, via a ref).
// We model the DOM node as a plain object with its own value.
const inputNode = { value: "" }; // the DOM element keeps its own value
// "Typing" writes straight to the node — no React state involved.
function type(text) { inputNode.value = text; }
type("hello@site.com"); // user types; React isn't notified
// On submit, you READ the current value once (this is what a ref does):
function handleSubmit() {
console.log("submitted:", inputNode.value);
}
handleSubmit(); // submitted: hello@site.com
// No re-render per keystroke — simpler, but you can't react as they type.3️⃣ Choosing — and the Switch Warning
Reach for controlled when you need live validation, conditional buttons, instant formatting, or when other UI depends on the value. Reach for uncontrolled for simple forms or when you must (file inputs are always uncontrolled). The cardinal sin is switching mid-life: if value starts undefined and later becomes a string, React warns.
// ❌ value is undefined on first render, then defined later -> warning!
const [name, setName] = useState(); // undefined to start
<input value={name} onChange={e => setName(e.target.value)} />
// ✅ Controlled: always a defined string
const [name, setName] = useState(""); // "" not undefined
<input value={name} onChange={e => setName(e.target.value)} />Fix it by initializing controlled state to a defined value (like ""), and never feed value={undefined}.
Reorder Challenge
These lines build a controlled input. Put them in a correct order:
A. return <input value={text} onChange={e => setText(e.target.value)} />;
B. const [text, setText] = useState("");
C. function NameField() {
D. }Quick Recall
1) Which prop makes an input controlled: value or defaultValue?
value (bound to state). defaultValue sets only the initial value of an uncontrolled input.
2) You start with useState() and bind it to value. What goes wrong?
The value starts undefined (uncontrolled) then becomes defined (controlled), triggering React's switch warning. Initialize to "".
3) Which input type MUST be uncontrolled?
A file input (<input type="file" />) — its value is read-only and can't be set from React state.
Common Errors (and the fix)
- "A component is changing an uncontrolled input to be controlled." — value went from undefined to defined. Initialize state to "".
- Input is read-only / can't type: you set value but no onChange. Controlled inputs need both.
- Using value with a ref: for uncontrolled inputs use defaultValue, not value.
- Reading state right after setState and expecting the new value: state updates are async; read e.target.value for the latest keystroke.
📋 Quick Reference
| Mode | Syntax |
|---|---|
| Controlled | <input value={v} onChange={e => setV(e.target.value)} /> |
| Uncontrolled | <input ref={r} defaultValue="" /> |
| Read uncontrolled | r.current.value |
| Avoid warning | useState("") // not useState() |
Mini-Challenge: Live Character Count
A controlled input is perfect when other UI depends on the value. Model an onChange that stores the text, then compute the remaining characters out of a 10-char limit. Predict the result before running.
let text = "";
function onChange(typed) { text = typed; } // controlled: state drives it
onChange("hello");
// your code here: compute remaining = 10 - text.length and log it (expect 5)🎉 Lesson Complete
- ✅ Controlled: value + onChange, React is the source of truth
- ✅ Uncontrolled: DOM holds the value, read it via a ref + defaultValue
- ✅ Controlled lets you validate/format every keystroke
- ✅ Never switch modes; initialize controlled state to a defined value
- ✅ File inputs are always uncontrolled
Practice quiz
What makes an input 'controlled' in React?
- It uses defaultValue
- It has no onChange handler
- Its value is driven by React state via value + onChange
- It is wrapped in a ref
Answer: Its value is driven by React state via value + onChange. A controlled input binds value={state} and updates state in onChange — React is the source of truth.
Where does an uncontrolled input keep its value?
- In the DOM, read on demand (usually via a ref)
- In React state
- In localStorage
- In a context provider
Answer: In the DOM, read on demand (usually via a ref). An uncontrolled input keeps its value in the DOM; you read it when needed, typically with a ref.
Which prop sets the initial value of an uncontrolled input?
- value
- initialValue
- startValue
- defaultValue
Answer: defaultValue. Uncontrolled inputs use defaultValue for the starting value; value would make it controlled.
A controlled input shows but you can't type in it. What's the likely cause?
- Missing defaultValue
- value is set but there's no onChange handler
- The input is inside a form
- Too many re-renders
Answer: value is set but there's no onChange handler. A controlled input needs both value and onChange; with value but no onChange it becomes read-only.
What triggers React's 'changing an uncontrolled input to be controlled' warning?
- An input's value going from undefined to a defined value (or vice versa)
- Using onChange
- Using defaultValue
- Rendering inside a list
Answer: An input's value going from undefined to a defined value (or vice versa). Switching value between undefined and defined flips the input's mode. Pick one and stick with it.
How do you avoid the controlled/uncontrolled switch warning?
- Start state as undefined
- Always use defaultValue and value together
- Initialize controlled state to a defined value like ""
- Remove onChange
Answer: Initialize controlled state to a defined value like "". Initialize controlled state to a defined value (e.g. useState("")) so value is never undefined.
Which input type must always be uncontrolled?
- A text input
- A file input
- A checkbox
- A select
Answer: A file input. A file input's value is read-only and can't be set from React state, so it must be uncontrolled.
When is a controlled input the better choice?
- For simple forms you read only on submit
- When integrating non-React code
- For file uploads
- When you need live validation, formatting, or other UI depending on the value
Answer: When you need live validation, formatting, or other UI depending on the value. Controlled inputs let you validate, format, or disable based on every keystroke and drive other UI.
How do you read an uncontrolled input's current value?
- From state
- From ref.current.value
- From e.preventDefault()
- From defaultValue
Answer: From ref.current.value. Attach a ref and read ref.current.value (e.g. on submit) to get the DOM-held value.
In a controlled input, why can you transform text as the user types?
- Because the DOM stores the value
- Because defaultValue formats it
- Because onChange computes the next state, and value only shows what state holds
- You cannot transform controlled input
Answer: Because onChange computes the next state, and value only shows what state holds. Since state drives value, your onChange can transform the input (uppercase, strip non-digits) before storing it.
Continue this course
- Previous: Forms and Controlled Components
- Next: Lazy Loading & Suspense — Code-split your app with React.lazy and Suspense
- Quick reference: React cheat sheet