Build a contact form — the name / email / message form at the bottom of almost every website. The user fills in three fields and hits send; in a real app the data is POSTed to a backend, and the form acknowledges success. This is the canonical exercise in controlled inputs: the framework, not the DOM, owns what's typed, so the values live in state and the inputs render from them.
// A self-contained component. No props.
function App(): JSX.Element;
Three fields (name, email, message) and a submit button; on submit, show a success acknowledgement.
fill in: Name = "Ada", Email = "[email protected]", Message = "Hi!"
click Send → form is replaced by: "Message sent ✓ Thanks, Ada…"
click "Send another" → fields cleared, form shown again
There is no real server in this exercise. Calling preventDefault() stops the
native page reload; "sending" just flips a flag and shows the success view.
value comes from state and its onChange writes back to state — the state is the single source of truth for what's typed.<form> submit reloads the page by default; call e.preventDefault() in the submit handler to stop that and handle it in JS.await fetch('/api/contact', …) and handle loading/error states.