Controlled Elements: How React Takes Ownership of Form Inputs

By default, form inputs store their value in the DOM. Controlled elements move that value into React state, making the form fully predictable and resettable.

June 18, 20263 min read2 / 2

I used to grab form values with document.querySelector('input').value.

Then I switched to React and kept doing the same thing, just inside a submit handler. It worked. Until it did not.

The input showed one value. My handler read another. The bug took thirty minutes to track down, and the fix was three lines.

That fix is controlled elements.

Why Inputs Are Different

A regular <div> is passive. You put text in it, React puts text in it. Either way, the DOM reflects whatever you last set.

A form input is different. It maintains its own internal state. When a user types into an <input>, the browser updates the element's value property directly, without going through React. The DOM and React are both managing the same element, but neither one knows what the other is doing.

This is the same "state in the DOM" problem that React was built to solve, just inside a form element instead of a counter button.

The Controlled Elements Technique

The solution is to make React the single owner of the input's value. Three steps.

Step 1: Create a piece of state.

TSX
const [description, setDescription] = useState('');

Step 2: Connect the state to the input's value prop.

TSX
<input type="text" value={description} />

Now React is in charge. The input can only show what description contains. The user can type all they want, but the displayed value will not change because the state is not updating yet.

Step 3: Listen for changes and update the state.

TSX
<input type="text" value={description} onChange={(e) => setDescription(e.target.value)} />

Now when the user types, the change event fires, the handler updates description, React re-renders, and the input shows the new value. The loop is complete.

A circular flow diagram: user types into input, change event fires, onChange calls setDescription with e.target.value, React re-renders, input displays new description value, back to user. ExpandA circular flow diagram: user types into input, change event fires, onChange calls setDescription with e.target.value, React re-renders, input displays new description value, back to user.

React owns the state. The DOM reflects it. They are always in sync.

The String Gotcha

e.target.value is always a string. Even for <input type="number"> and <select> elements, the value comes back as a string.

TSX
const [quantity, setQuantity] = useState(1); // number <select value={quantity} onChange={(e) => setQuantity(e.target.value)} // bug: sets a string />

After the first change, quantity becomes "2" instead of 2. Any arithmetic downstream produces "21" instead of 3.

Fix it at the update site:

TSX
onChange={(e) => setQuantity(Number(e.target.value))}

Convert before setting state. Do not wait until you need to use the value.

The Reset Payoff

Because React owns the form state, resetting it is just resetting state variables.

TSX
function handleSubmit(e: React.FormEvent) { e.preventDefault(); const newItem = { description, quantity, packed: false, id: Date.now() }; // do something with newItem setDescription(''); // form resets setQuantity(1); // form resets }

No form.reset(). No document.querySelectorAll('input'). The form goes back to its initial state because the state it was displaying went back to its initial state.

This is the exact payoff of the controlled elements model: the form is just a view of your state. Change the state, the form changes with it.

Uncontrolled Inputs

An uncontrolled input is one where the DOM, not React, owns the value. You read it out of the DOM using a ref when you need it.

TSX
const inputRef = useRef<HTMLInputElement>(null); function handleSubmit() { console.log(inputRef.current?.value); // reading from DOM } <input ref={inputRef} type="text" />

This works fine for simple cases like a search box you only need to read on submit. It does not work when you need the value during typing, for validation, dependent fields, or live previews.

For any form that needs to respond to user input as it happens, use controlled elements. For simple read-on-submit scenarios, uncontrolled is acceptable.

The Essentials

  1. Form inputs maintain their own state in the DOM by default. Controlled elements override this by binding value to a React state variable, making React the single source of truth.
  2. Three steps: create state, set value={state} on the element, update state with onChange={(e) => setState(e.target.value)}. All three are required. Missing the onChange makes the input read-only.
  3. e.target.value is always a string. Convert with Number() before setting state for numeric inputs. Reset the form by resetting the state variables.

Further Reading and Watching