Forms & Controlled Inputs
Wire form fields to state so React is the single source of truth — the controlled-component pattern.
In plain HTML, an <input> keeps its own value in the DOM. In React we usually make inputs controlled: their value comes from state, and every keystroke updates that state. React becomes the single source of truth for what's in the field.
import { useState } from 'react';
function NameField() {
const [name, setName] = useState('');
return (
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="Your name"
/>
);
}Two bindings make it controlled: value={name} (state → input) and onChange (input → state). Because the value always mirrors state, you can validate, format, or disable the submit button based on it.
value without onChange = read-only
If you set value={name} but forget onChange, React locks the field — typing does nothing, and you'll see a console warning. Always pair value with an onChange (or use defaultValue for an uncontrolled field).
#Handling submit
function Form() {
const [email, setEmail] = useState('');
function handleSubmit(e) {
e.preventDefault(); // stop the full-page reload
console.log('submit', email);
}
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={e => setEmail(e.target.value)} />
<button>Send</button>
</form>
);
}What makes an input a *controlled* component?
Key takeaways
- Controlled inputs derive their value from state and update it on every change.
- Bind both `value={state}` and `onChange={e => setState(e.target.value)}`.
- value without onChange makes the field read-only (and warns).
- Call `e.preventDefault()` in onSubmit to stop the browser's full-page reload.
Typing in this input does nothing. Why?
function Field() {
const [name, setName] = useState('');
return <input value={name} />;
}Submitting this form reloads the whole page. What's missing?
function Form() {
function handleSubmit(e) {
console.log('sent');
}
return <form onSubmit={handleSubmit}><button>Go</button></form>;
}Complete the change handler that keeps state in sync with the input.
<input value={q} onChange={e => setQ(e.target.)} />
Build a controlled <textarea> whose current character count is shown below it (e.g. '12 characters'). Store the text in state.
Try it live — edit the code and hit Run to see it rendered: