2 · Data and boundaries · 30 MIN
Controlled forms and useful validation
A controlled input has a single source of truth in state.
The input’s value comes from state and onChange updates that state for the next render. Submitting the form also works when the learner presses Enter. Validation is shown next to the field and announced through a status message; a label provides an accessible name. Client validation improves feedback but does not authorize or validate an eventual server write. A real endpoint must repeat its own validation and derive ownership from an authenticated session.
Submit with Enter as well as clicking the button.
Read the example
import {useState,type FormEvent} from 'react';
export default function App() {
const [title,setTitle]=useState(''),[message,setMessage]=useState('');
function submit(e:FormEvent<HTMLFormElement>) {
e.preventDefault();
setMessage(title.trim().length<3?'Use at least three characters.':'Plan created: '+title.trim());
}
return <form onSubmit={submit}><h1>Create a study plan</h1><label htmlFor="title">Plan title</label>
<input id="title" value={title} maxLength={80} aria-describedby="feedback" onChange={e=>setTitle(e.target.value)}/>
<button>Create plan</button><p id="feedback" role="status">{message}</p></form>;
}Check the expected output
Submitting fewer than three trimmed characters shows validation. A valid title shows Plan created: followed by the title.
Your challenge
Add a numeric weekly goal with a 1–20 range and announce field-specific errors without clearing valid input.
Solution cost: O(m) trimming and validation for title length m. time · O(m) form text. space
Common trap
Disabled submit buttons alone do not explain what the learner needs to fix.
Study the project implementation
import {useState,type FormEvent} from 'react';
export default function App() {
const [title,setTitle]=useState(''),[message,setMessage]=useState('');
function submit(e:FormEvent<HTMLFormElement>) {
e.preventDefault();
setMessage(title.trim().length<3?'Use at least three characters.':'Plan created: '+title.trim());
}
return <form onSubmit={submit}><h1>Create a study plan</h1><label htmlFor="title">Plan title</label>
<input id="title" value={title} maxLength={80} aria-describedby="feedback" onChange={e=>setTitle(e.target.value)}/>
<button>Create plan</button><p id="feedback" role="status">{message}</p></form>;
}Further reading: Official documentation
Next lesson: Derived state and searchable lists →