Handle clicks and typing, and build a controlled form whose state drives validation and submission.
Why it matters
Sign-up pages, search boxes, filters, checkout steps: most of the interaction in an app is people clicking and typing. React handles events with props such as onClick and onChange, and handles form fields with controlled components, where React state is the single source of truth for what is in each input.
Handling events
Pass a function to an event prop. React calls it with an event object:
function SaveButton() {
function handleClick(event) {
console.log("Saved!", event.type); // "click"
}
return <button onClick={handleClick}>Save</button>;
}
- Event props are camelCase:
onClick,onChange,onSubmit,onKeyDown,onFocus. - Pass the function, do not call it:
onClick={handleClick}, notonClick={handleClick()}. - To pass arguments, wrap it:
onClick={() => removeItem(item.id)}. event.preventDefault()stops the browser's default action (following a link, submitting a form).event.stopPropagation()stops the event bubbling to parent elements.
React's event object wraps the browser's native event and works the same across browsers.
Controlled inputs
In plain HTML an input keeps its own value. In React you usually keep the value in state and feed it back to the input:
function NameField() {
const [name, setName] = React.useState("");
return (
<label>
Name
<input value={name} onChange={(e) => setName(e.target.value)} />
</label>
);
}
This loop is the core of controlled components:
- The input shows
value={name}. - The user types; React fires
onChangewith the new text ine.target.value. - You call
setName, React re-renders, and the input shows the new value.
If you set value without onChange, the input becomes read-only, and React warns you. Because the value lives in state, you can validate, transform (for example e.target.value.toUpperCase()), reset or pre-fill it easily.
In React, onChange fires on every keystroke, like the DOM input event, not only when the field loses focus.
Other field types
<textarea value={bio} onChange={(e) => setBio(e.target.value)} />
<select value={plan} onChange={(e) => setPlan(e.target.value)}>
<option value="free">Free</option>
<option value="pro">Pro</option>
</select>
<input
type="checkbox"
checked={agree}
onChange={(e) => setAgree(e.target.checked)}
/>
Checkboxes use checked and e.target.checked instead of value.
One object for many fields
For larger forms, keep the fields in one object and use the input's name to update the right key:
const [form, setForm] = React.useState({ name: "", email: "" });
function handleChange(e) {
const { name, value } = e.target;
setForm((f) => ({ ...f, [name]: value }));
}
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
Validation and submitting
Validation rules are derived from state, so compute them during render:
const valid = form.name.trim() !== "" && form.email.includes("@");
function handleSubmit(e) {
e.preventDefault(); // stay on the page
if (!valid) return;
onSignup({ name: form.name.trim(), email: form.email.trim() });
setForm({ name: "", email: "" }); // clear the fields
}
return (
<form onSubmit={handleSubmit}>
...
<button type="submit" disabled={!valid}>Sign up</button>
</form>
);
Using a real <form> with onSubmit is best in apps: pressing Enter submits and screen readers understand the structure. (The exercise preview on this site blocks form submission, so the Do tab uses a type="button" with onClick instead.)
Client-side checks are for a pleasant experience only. The server must validate again, because anyone can send requests without your form.
Accessible forms
- Every input needs a label. Wrap the input in
<label>or connect them withhtmlForandid. - Show errors as text near the field, not just a red border, and use
role="alert"orrole="status"for messages that appear after an action so screen readers announce them. - A disabled button gives no hint why. For important forms, also show what is missing.
Uncontrolled inputs
Sometimes you only need the value at submit time. You can let the DOM keep it and read it with a ref (React.useRef) or new FormData(e.target). That is an uncontrolled input. Controlled inputs are the default choice when the UI reacts to what the user types: live validation, disabling buttons, filtering a list.
Common mistakes
valuewithoutonChange(read-only field) or switching an input betweenvalue={undefined}and a string (React warns about changing from uncontrolled to controlled). Start with"".- Forgetting
e.preventDefault()inonSubmit, so the page reloads. - Using
valueon a checkbox instead ofchecked. - Trusting client-side validation as security.
Recap
- Event props take a function; use arrow functions to pass arguments.
- Controlled inputs:
valuefrom state plusonChangethat sets state. - Derive validity from state; clear or reset by setting state.
- Label every field and announce results; validate again on the server.
Try it
Open the Do tab to build a controlled sign-up form whose button enables only when the fields are valid, and that clears itself after a successful sign-up.