Overview
Controlled inputs and event handling are core React skills because most real interfaces respond to user input: typing in forms, clicking buttons, selecting options, submitting data, dismissing dialogs, and changing filters. React handles these interactions declaratively. Instead of manually reading and mutating DOM fields, a component stores important input values in state and renders the UI from that state.
function SearchBox() {
const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
);
}
This is a controlled input: React state is the source of truth, and the input displays whatever value React passes to it. When the user types, onChange updates state, React re-renders, and the input receives the new value.
Event handling is the other half of the pattern. React lets you pass functions to JSX event props such as onClick, onChange, onSubmit, onKeyDown, and onBlur. These handlers run in response to user interactions and often update state, call parent callbacks, prevent default browser behavior, or coordinate UI transitions.
For interviews, this topic matters because controlled inputs reveal whether a developer understands React's one-way data flow, state updates, event handler timing, form submission, accessibility, and common pitfalls such as calling handlers during render or switching an input between controlled and uncontrolled modes.
Core Concepts
Event Handlers
An event handler is a function passed to a JSX event prop.
function SaveButton() {
function handleClick() {
console.log("Saving...");
}
return <button onClick={handleClick}>Save</button>;
}
The handler is passed, not called. This is correct:
<button onClick={handleClick}>Save</button>
This is wrong:
<button onClick={handleClick()}>Save</button>
The second version runs handleClick during render instead of waiting for a click.
Inline handlers are fine for short logic:
<button onClick={() => setOpen(true)}>Open</button>
For more complex logic, use a named handler:
function handleSubmitClick() {
validateForm();
submitForm();
}
Handler Naming Conventions
By convention:
- Handler functions inside a component often start with
handle. - Handler props passed into a component often start with
on.
function Toolbar({
onSave,
onCancel,
}: {
onSave: () => void;
onCancel: () => void;
}) {
return (
<div>
<button onClick={onSave}>Save</button>
<button onClick={onCancel}>Cancel</button>
</div>
);
}
Use domain-specific names when they make intent clearer:
<VideoControls
onPlayMovie={playMovie}
onUploadImage={uploadImage}
/>
This keeps parent-child contracts meaningful. A reusable Button might expose onClick, but a feature component should often expose onSelectUser, onSubmitOrder, or onCloseDialog.
Event Objects
React passes an event object to event handlers.
function TextInput() {
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
console.log(event.target.value);
}
return <input onChange={handleChange} />;
}
Common event fields and methods:
event.target: the element where the event originated.event.currentTarget: the element the handler is attached to.event.preventDefault(): prevents default browser behavior.event.stopPropagation(): stops the event from bubbling to parent handlers.
In TypeScript, currentTarget is often easier to type safely because it refers to the element that owns the handler:
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setEmail(event.currentTarget.value);
}
Event Propagation
Most React events bubble up the component tree. If a child and parent both listen for clicks, the child handler runs first, then the parent handler.
function Toolbar() {
return (
<div onClick={() => console.log("toolbar")}>
<button onClick={() => console.log("button")}>
Save
</button>
</div>
);
}
Clicking the button logs both messages.
To stop the event from reaching the parent:
function StopButton({ onClick }: { onClick: () => void }) {
return (
<button
onClick={(event) => {
event.stopPropagation();
onClick();
}}
>
Save
</button>
);
}
Use stopPropagation deliberately. Often, explicit callback chains are easier to trace than relying on bubbling.
Preventing Default Form Behavior
HTML forms submit by default, which may reload the page. React form handlers usually prevent that default behavior and handle submission in JavaScript.
function LoginForm() {
const [email, setEmail] = useState("");
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
submitLogin(email);
}
return (
<form onSubmit={handleSubmit}>
<input
value={email}
onChange={(event) => setEmail(event.currentTarget.value)}
/>
<button type="submit">Log in</button>
</form>
);
}
Prefer onSubmit on the form over onClick on the submit button. It supports Enter key submission and works better with browser form semantics.
Controlled Inputs
A controlled input receives its value from React state and reports changes through an event handler.
function NameField() {
const [name, setName] = useState("");
return (
<input
value={name}
onChange={(event) => setName(event.currentTarget.value)}
/>
);
}
The value shown in the input is always the name state value. Typing triggers onChange, the handler updates name, and React re-renders with the new value.
Controlled inputs are useful when you need to:
- Validate as the user types.
- Enable or disable buttons based on input.
- Format or normalize values.
- Reset a form from state.
- Submit values from React state.
- Keep multiple fields or components synchronized.
Controlled Textareas
In React, a controlled <textarea> uses value and onChange, not children text.
function CommentBox() {
const [comment, setComment] = useState("");
return (
<textarea
value={comment}
onChange={(event) => setComment(event.currentTarget.value)}
/>
);
}
Use defaultValue for an uncontrolled initial value:
<textarea defaultValue="Initial comment" />
Do not mix value and defaultValue for the same field.
Controlled Selects
A controlled <select> also uses value and onChange.
function RoleSelect() {
const [role, setRole] = useState("user");
return (
<select
value={role}
onChange={(event) => setRole(event.currentTarget.value)}
>
<option value="admin">Admin</option>
<option value="user">User</option>
<option value="guest">Guest</option>
</select>
);
}
For multiple select, the selected value is usually modeled as an array:
function TagSelect() {
const [tags, setTags] = useState<string[]>([]);
return (
<select
multiple
value={tags}
onChange={(event) => {
const selected = Array.from(
event.currentTarget.selectedOptions,
(option) => option.value
);
setTags(selected);
}}
>
<option value="react">React</option>
<option value="typescript">TypeScript</option>
<option value="testing">Testing</option>
</select>
);
}
Checkboxes and Radio Buttons
Text inputs use value; checkboxes and radio buttons use checked.
function NewsletterCheckbox() {
const [subscribed, setSubscribed] = useState(false);
return (
<label>
<input
type="checkbox"
checked={subscribed}
onChange={(event) => setSubscribed(event.currentTarget.checked)}
/>
Subscribe
</label>
);
}
Radio buttons commonly share one state value:
function PlanPicker() {
const [plan, setPlan] = useState("basic");
return (
<fieldset>
<label>
<input
type="radio"
name="plan"
value="basic"
checked={plan === "basic"}
onChange={(event) => setPlan(event.currentTarget.value)}
/>
Basic
</label>
<label>
<input
type="radio"
name="plan"
value="pro"
checked={plan === "pro"}
onChange={(event) => setPlan(event.currentTarget.value)}
/>
Pro
</label>
</fieldset>
);
}
Common checkbox mistake:
setSubscribed(event.currentTarget.value);
Use checked, not value, for booleans.
Controlled vs Uncontrolled Inputs
A controlled input is driven by React state:
<input value={name} onChange={(event) => setName(event.currentTarget.value)} />
An uncontrolled input lets the DOM manage the current value:
<input defaultValue="Ava" />
Controlled inputs are best when React needs to know and control the value. Uncontrolled inputs are fine for simple forms, integration with non-React code, or values read only on submit through FormData or refs.
Do not switch a field between controlled and uncontrolled during its lifetime:
<input value={maybeName} onChange={handleChange} />
If maybeName starts as undefined and later becomes a string, React treats that as switching modes. Use a stable fallback:
<input value={maybeName ?? ""} onChange={handleChange} />
Synchronous Updates for Controlled Inputs
A controlled input should synchronously update its backing state in onChange.
function NameField() {
const [name, setName] = useState("");
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setName(event.currentTarget.value);
}
return <input value={name} onChange={handleChange} />;
}
Avoid delaying the state update that controls the input:
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setTimeout(() => {
setName(event.currentTarget.value);
}, 100);
}
This can make the input feel broken because React keeps rendering the old value while the user types. If expensive work is needed, update the input value immediately and defer the expensive derived work separately.
Form State Shape
For small forms, separate state variables are readable:
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
For larger forms, object state can reduce repetition:
type LoginForm = {
email: string;
password: string;
};
function LoginForm() {
const [form, setForm] = useState<LoginForm>({
email: "",
password: "",
});
function updateField<K extends keyof LoginForm>(key: K, value: LoginForm[K]) {
setForm((current) => ({
...current,
[key]: value,
}));
}
return (
<form>
<input
value={form.email}
onChange={(event) => updateField("email", event.currentTarget.value)}
/>
<input
type="password"
value={form.password}
onChange={(event) => updateField("password", event.currentTarget.value)}
/>
</form>
);
}
When using object state, remember that setting state replaces the object. Copy unchanged fields with spread.
Declarative Form UI
React encourages describing the form's visual states, then deriving the UI from state.
type Status = "idle" | "submitting" | "success" | "error";
function ContactForm() {
const [status, setStatus] = useState<Status>("idle");
const [message, setMessage] = useState("");
const isSubmitting = status === "submitting";
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setStatus("submitting");
try {
await sendMessage(message);
setStatus("success");
} catch {
setStatus("error");
}
}
return (
<form onSubmit={handleSubmit}>
<textarea
value={message}
disabled={isSubmitting}
onChange={(event) => setMessage(event.currentTarget.value)}
/>
<button disabled={isSubmitting || message.trim() === ""}>
Send
</button>
{status === "error" && <p role="alert">Failed to send.</p>}
</form>
);
}
The UI follows state. The code does not manually enable, disable, show, or hide DOM nodes imperatively.
Accessibility and Semantic Events
Use the right HTML element for the interaction.
Good:
<button onClick={onClose}>Close</button>
Risky:
<div onClick={onClose}>Close</div>
A real button supports keyboard interaction, focus behavior, disabled state, and semantic meaning. If a clickable element is truly a button, use <button>.
For forms:
- Use
<label htmlFor="fieldId">or wrap the input in a label. - Use
type="submit"for submit buttons. - Use
type="button"for non-submit buttons inside forms. - Use
aria-invalid,aria-describedby, androle="alert"where appropriate for validation errors.
Common Mistakes
Common mistakes include:
- Calling a handler during render:
onClick={handleClick()}. - Passing
valuewithoutonChangefor an editable field. - Using
valueinstead ofcheckedfor checkboxes. - Switching an input from uncontrolled to controlled by using
undefinedornull. - Using
defaultValueand expecting later state changes to update the field. - Handling form submission only on a button click instead of
onSubmit. - Forgetting
event.preventDefault()for JavaScript form submission. - Overusing
stopPropagationinstead of designing explicit callbacks. - Using
<div onClick>when a semantic<button>is appropriate. - Doing expensive validation synchronously on every keystroke without considering responsiveness.
Best Practices
Use these rules of thumb:
- Use controlled inputs when React needs to validate, submit, reset, or coordinate field values.
- Use
valuefor text-like fields andcheckedfor checkbox/radio booleans. - Update controlled input state synchronously in
onChange. - Use
onSubmiton forms and callpreventDefaultfor JavaScript submission. - Pass event handlers as functions, not function calls.
- Name feature-level callback props after user intent.
- Keep side effects in event handlers, not render logic.
- Prefer semantic HTML elements for accessibility.
- Keep form state as simple as possible.