Handling events in React
React's event system looks like plain DOM event handling with different capitalization, and mostly is — but it wraps every native event in its own object, and it handles attaching/detaching listeners for you entirely, which changes a few things worth knowing explicitly.
4 min read
The basic shape: camelCase, and a function, not a string
// Plain HTML/DOM
<button onclick="handleClick()">Click</button>
// React JSX
<button onClick={handleClick}>Click</button>React's event props use camelCase (onClick, not onclick) and take an actual function reference as their value, not a string of code to eval-style execute the way an inline HTML onclick="..." attribute does. This is consistent with everything else about JSX props being plain JavaScript values (from the JSX lesson) — onClick={handleClick} passes the function itself; React calls it when the click actually happens.
The classic mistake: calling the function instead of passing it
// WRONG — this calls handleClick() immediately, during render, not on click
<button onClick={handleClick()}>Click</button>
// RIGHT — this passes the function itself, to be called later, on click
<button onClick={handleClick}>Click</button>
// Also right — an inline arrow function, useful when you need to pass arguments
<button onClick={() => handleDelete(item.id)}>Delete</button>onClick={handleClick()} calls handleClick right now, during render, and whatever it returns becomes the actual value assigned to onClick — almost never what's wanted, and a genuinely common early mistake, since JavaScript itself doesn't flag this as any kind of error (it's completely valid syntax, it just does something different from what was intended). Passing an inline arrow function, () => handleDelete(item.id), is the standard way to call a handler with arguments — the arrow function itself is what gets passed as the actual onClick value, and it's the arrow function, not handleDelete, that React actually calls on click.
SyntheticEvent: React's own wrapper around the real browser event
function Form() {
function handleSubmit(event) {
event.preventDefault(); // stops the browser's default full-page-reload form submit
console.log(event.target); // the actual DOM element that triggered this
}
return <form onSubmit={handleSubmit}>...</form>;
}The event object a React handler receives isn't the raw browser event directly — it's a SyntheticEvent, React's own cross-browser-consistent wrapper around it, with the same familiar API (preventDefault(), stopPropagation(), target) that native DOM events already have. This exists mainly for historical browser-consistency reasons (smoothing over real differences between how different browsers used to implement events) — in modern React, it behaves close enough to a native event that the distinction rarely matters day to day, but it's worth knowing it's technically a wrapper, not the literal native event object, if a deeper DOM API is ever needed directly.
React attaches (and removes) listeners for you — no manual cleanup
// Plain DOM: you own the listener's lifecycle yourself
button.addEventListener("click", handleClick);
// ...later, you'd have to remember to call:
button.removeEventListener("click", handleClick);// React: the listener's lifecycle is tied to the component automatically
function Button() {
return <button onClick={handleClick}>Click</button>;
// React attaches this while Button is on screen, and removes it
// automatically the moment Button is removed — no manual cleanup needed
}With plain DOM APIs, forgetting to remove an event listener when the element it's attached to goes away is a real, common source of memory leaks and "handler fires on an element that shouldn't exist anymore" bugs. React manages this automatically — the event listener's lifecycle is tied directly to the component's own lifecycle (covered properly once useEffect is introduced later in this domain), attached while the component is mounted, removed the instant it isn't, with zero manual bookkeeping required from the code you actually write.
Controlled inputs: state and the DOM value, kept in sync explicitly
function NameInput() {
const [name, setName] = useState("");
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}An input with both a value prop and an onChange handler is a controlled input — its displayed value is driven entirely by React state, not by the browser's own internal input state. Every keystroke fires onChange, which updates name via setName, which triggers a re-render, which sets the input's value right back to (the now-updated) name — the input's on-screen text only ever changes because React explicitly told it to, never because the browser updated it independently. This is the standard, idiomatic React pattern for form inputs specifically because it makes the current value of every field always readable directly from state, rather than needing to reach into the actual DOM to ask an input what it currently contains.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What's the actual bug in `<button onClick={handleClick()}>Click</button>`?
2. What is a SyntheticEvent in React?
3. What makes an input a 'controlled input' in React?