Handling Events
Interactivity in React comes down to responding to events—a click, a keystroke, a form submission. You wire these up by passing functions to special JSX props such as onClick and onChange. React handles the low-level DOM plumbing for you and gives every handler a consistent, cross-browser event object, so the same code behaves the same everywhere.
Event props are camelCase
In plain HTML you write attributes like onclick in lowercase. In JSX, event props are camelCased: onClick, onChange, onSubmit, onMouseEnter, onKeyDown, and so on. The value you assign is a JavaScript expression inside curly braces, not a string.
function SaveButton() {
return <button onClick={() => console.log("saved")}>Save</button>;
}
The most common event props map directly to familiar DOM events:
| JSX prop | Fires when… | Common element |
|---|---|---|
onClick | the element is clicked | <button> |
onChange | an input’s value changes | <input> |
onSubmit | a form is submitted | <form> |
onKeyDown | a key is pressed | any focusable |
onMouseEnter | the pointer enters the element | any element |
Pass the function, don’t call it
This is the single most common beginner mistake. You must pass a function reference, not the result of calling it.
function Counter() {
function handleClick() {
console.log("clicked");
}
// ✅ correct — pass the function; React calls it on click
return <button onClick={handleClick}>Click me</button>;
}
If you write onClick={handleClick()} with parentheses, JavaScript calls handleClick immediately during render and assigns its return value (usually undefined) to onClick. The handler then runs once at render time instead of on each click.
Rule of thumb:
onClick={handleClick}(no parens) for a named handler, oronClick={() => handleClick()}(an arrow wrapper) when you need to control how it’s called.
The synthetic event object
When an event fires, React passes your handler a SyntheticEvent—a thin, cross-browser wrapper around the native DOM event. It exposes the same API you already know: event.target, event.preventDefault(), event.stopPropagation(), event.key, and more. Because the interface is normalized, you don’t have to worry about browser quirks.
function NameField() {
function handleChange(event) {
// event.target is the <input>; .value is its current text
console.log(event.target.value);
}
return <input onChange={handleChange} placeholder="Type your name" />;
}
By convention the parameter is named event or just e. Access the native event underneath with event.nativeEvent if you ever need it.
Passing arguments to a handler
Often a handler needs extra data—which item was clicked, for instance. Wrap the call in an inline arrow function so the argument is supplied only when the event fires:
function ItemList({ items, onDelete }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item.label}
<button onClick={() => onDelete(item.id)}>Delete</button>
</li>
))}
</ul>
);
}
The arrow function () => onDelete(item.id) is the reference passed to onClick; onDelete runs with the right id only on click. If you need the event too, include it: onClick={(e) => onDelete(item.id, e)}.
preventDefault and stopPropagation
Some elements have built-in browser behavior. A <form> reloads the page on submit; a link navigates. Call event.preventDefault() to suppress that default behavior and handle things yourself.
import { useState } from "react";
function LoginForm() {
const [email, setEmail] = useState("");
function handleSubmit(event) {
event.preventDefault(); // stop the full-page reload
console.log("Submitting", email);
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Log in</button>
</form>
);
}
Output:
Submitting [email protected]
event.stopPropagation() is different: it stops the event from bubbling up to parent handlers. Use it when an inner click shouldn’t trigger an outer one.
function Card({ onOpen }) {
return (
<div onClick={onOpen}>
<h3>Project</h3>
<button
onClick={(e) => {
e.stopPropagation(); // don't trigger the card's onOpen
console.log("liked");
}}
>
Like
</button>
</div>
);
}
Inline vs named handlers
Both styles are valid; choose by readability.
| Style | Looks like | Best for |
|---|---|---|
| Inline | onClick={() => setOpen(true)} | one-liners, passing arguments |
| Named | onClick={handleOpen} | multi-step logic, reuse, easier testing |
Inline handlers keep trivial logic close to the markup. Once a handler grows beyond a line or two—or you reuse it—extract a named function (conventionally prefixed handle) for clarity.
Best Practices
- Pass a function reference (
onClick={handleClick}), never a call (onClick={handleClick()}). - Name handlers with a
handleprefix and name the props that accept them with anonprefix. - Use an inline arrow wrapper to pass arguments:
onClick={() => doThing(id)}. - Call
event.preventDefault()on form submits to stop the page reload. - Reach for
event.stopPropagation()only when you genuinely need to stop bubbling—overusing it makes behavior hard to trace. - Extract inline handlers into named functions once the logic exceeds a line or two.