Forms are how people give data to your app: they sign up, search, write comments and pay. In plain HTML the browser keeps the text typed in each input. In React we usually keep it in state instead, so the screen and the data always agree. This lesson teaches the most common way, called controlled inputs.
Controlled input: the idea
A controlled input gets its value from React state, and tells React about every change. Two props make this work: value (what to show) and onChange (what to do when the person types).
function NameForm() {
const [name, setName] = useState('');
return (
<form>
<label htmlFor="name">Your name</label>
<input
id="name"
value={name}
onChange={(event) => setName(event.target.value)}
/>
<p>You typed: {name}</p>
</form>
);
}
Follow the loop: the person types a letter, onChange runs, setName saves the new text in state, React draws again, and the input shows the new value. The state is the single source of truth. If you set value but forget onChange, the input becomes read-only and React warns you.
Do you see the label with htmlFor? It connects the text to the input. Clicking the label focuses the input, and screen readers read the label out. Always add one. A placeholder is not a replacement for a label.
Many fields, one state object
A form with ten fields should not need ten useState calls and ten handlers. Use one object, and give each input a name. One handler can then update any field:
function SignUpForm() {
const [form, setForm] = useState({ name: '', email: '', country: 'India' });
function handleChange(event) {
const { name, value } = event.target;
setForm({ ...form, [name]: value });
}
function handleSubmit(event) {
event.preventDefault();
console.log('Form data:', form);
}
return (
<form onSubmit={handleSubmit}>
<input name="name" value={form.name} onChange={handleChange} placeholder="Name" />
<input name="email" type="email" value={form.email} onChange={handleChange} placeholder="Email" />
<button type="submit">Sign up</button>
</form>
);
}
The strange [name]: value is a computed property name. It uses the value of the variable name (such as 'email') as the key. And ...form copies all the old fields first, so only one field is replaced. Remember the rule: never change state directly.
Checkboxes
A checkbox uses checked instead of value, and you read event.target.checked:
const [agree, setAgree] = useState(false);
<label>
<input type="checkbox" checked={agree} onChange={(e) => setAgree(e.target.checked)} />
I accept the terms
</label>
<button disabled={!agree}>Continue</button>
The disabled prop is tied to state, so the button works only after the box is ticked. This is a small taste of how state controls the page.
Select (dropdown)
In React a select also uses value on the select element itself, not selected on each option:
<select value={country} onChange={(e) => setCountry(e.target.value)}>
<option value="India">India</option>
<option value="UK">United Kingdom</option>
<option value="Canada">Canada</option>
</select>
Radio buttons
Radio buttons share one state value. Each radio is checked when its own value equals the state:
const [plan, setPlan] = useState('free');
{['free', 'pro'].map((p) => (
<label key={p}>
<input type="radio" name="plan" value={p} checked={plan === p} onChange={() => setPlan(p)} />
{p}
</label>
))}
Textarea
In HTML, a textarea has its text between tags. In React it is like an input: use value and onChange, and write it as a self-closing tag or with value:
<textarea value={message} onChange={(e) => setMessage(e.target.value)} rows={4} />
Simple validation
Validation means checking that what the person typed makes sense. Check on submit, and show a clear message next to the field. Keep the errors in state:
function ContactForm() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
function handleSubmit(event) {
event.preventDefault();
if (!email.includes('@')) {
setError('Please enter a valid email address.');
return;
}
setError('');
console.log('Sending', email);
}
return (
<form onSubmit={handleSubmit} noValidate>
<label htmlFor="email">Email</label>
<input id="email" value={email} onChange={(e) => setEmail(e.target.value)}
aria-invalid={error ? 'true' : 'false'} aria-describedby="email-error" />
{error && <p id="email-error" role="alert">{error}</p>}
<button type="submit">Send</button>
</form>
);
}
aria-invalidandaria-describedbyhelp screen readers know that the field has a problem and what it is.role="alert"makes screen readers read the message when it appears.- Write messages that help: say what to do, not only what went wrong.
- Always validate again on the server. The browser can be bypassed, so client checks are only a convenience.
Clearing the form after submit
Because the form lives in state, clearing it is easy: set the state back to the starting value. Keep that value in one constant so you can reuse it.
const empty = { name: '', email: '' };
const [form, setForm] = useState(empty);
function handleSubmit(event) {
event.preventDefault();
save(form);
setForm(empty);
}
Controlled or uncontrolled?
There is another way: uncontrolled inputs, where the browser keeps the value and you read it only when needed, with a ref or with FormData. It is shorter for simple forms and for very large forms. Lesson 19 shows it. For most forms where you check the text as people type, controlled inputs are the easiest to understand.
Common mistakes
- Setting
valuewithoutonChange: the field becomes impossible to type in. - Starting with
undefined:useState()with no value makes the input uncontrolled, then controlled, and React warns. Start with''. - Changing the form object directly instead of copying it.
- Using
onClickon the submit button instead ofonSubmiton the form. - Forgetting the label, which hurts people who use screen readers.
Walk through a keystroke
Controlled inputs are easier to trust when you follow one key press from start to finish. Say the person types the letter “A” in an empty name field.
- The browser fires a change event on the input.
- React calls your
onChangehandler with the event.event.target.valueis nowA. - Your handler calls
setName('A'). React stores the new value and plans a draw. - React calls your component again. This time
nameisA. - The input is drawn with
value={name}, so it showsA. Everything that usesname, such as a counter of characters, shows the new value too.
This happens fast enough that you never notice it. The key idea is that the input never keeps its own text. It always shows what is in state. That is why the state and the screen cannot disagree, and why you can do things like change the text to capitals as people type, limit the length, or clear the field from code.
A small example: capital letters and a limit
<input
value={code}
onChange={(e) => setCode(e.target.value.toUpperCase().slice(0, 6))}
placeholder="Enter a six letter code"
/>
The text is changed before it is saved in state, so the field always shows capital letters and never more than six of them. With a normal uncontrolled input you would need extra code to do this. Because the input is controlled, it is one line.
Questions beginners often ask
Do I need a library for forms? Not for small forms. Controlled inputs and a simple validation function are enough. Look at a library when your forms are large, have many steps or have many rules.
Why does my number input give text? The value of every input is text, even for type="number". Convert it with Number(value) when you need a number, and think about what to do when the field is empty.
How do I show a password? Keep a true or false state, such as show, and set type={show ? "text" : "password"}. Add a button with a clear label like “Show password”, and tell screen readers its state with aria-pressed.
How do I stop people from sending a form twice? Keep a sending state. Set it to true at the start of submit, disable the button while it is true, and set it to false when the request ends, whether it worked or not.
Your task
- Build a sign-up form with name, email, a country dropdown and a checkbox “I agree”. Use one state object and one change handler.
- Disable the Submit button until the box is ticked.
- On submit, check that the name is not empty and the email has an
@. Show an error message next to the field. - After a good submit, show “Thanks, NAME!” and clear the form.
How to check it worked
- Every field can be typed in, and the page shows the changes at once.
- Errors appear next to the field and disappear after a correct submit.
- You can reach every field and the button using only the Tab key.
Summary
- A controlled input has
valuefrom state andonChangethat updates state. - Use one state object and
[name]: valueto handle many fields with one function. - Checkboxes use
checked, selects usevalueon the select, and radios compare their value to state. - Validate on submit with clear messages, add labels, and check again on the server.