Lesson 1 of 8
Events in React
"The user interface is the sum of what the user does and what the machine does in return." — Bret Victor
Every interface you have ever loved was a conversation: you click, it responds; you type, it reacts. In plain JavaScript that conversation is wired with addEventListener — you find a DOM node, attach a callback, and remember to tear it down later. React throws that whole ritual away. You describe what should happen when the user acts, right there in the markup, and React handles the wiring, the cross-browser quirks, and the cleanup. Events are where a static tree of components becomes a living application — and they are the first thing an interviewer probes when they want to know whether you actually understand React or just memorised JSX.
When a user clicks, types, or submits, how do you tell React to run your code — and why does onClick={handleClick()} fire the moment the page loads instead of when you click?
Core Concepts
You attach event handlers by passing a function to a JSX prop. The prop is named after the DOM event, but in camelCase: the DOM click event becomes onClick, change becomes onChange, submit becomes onSubmit, keydown becomes onKeyDown. The value you pass is a function — a reference React will call later, when the event actually happens.
That last sentence is the whole game, and it hides the single most common React bug there is:
Write onClick={handleClick} and React holds onto your function and invokes it on each click. Write onClick={handleClick()} — with parentheses — and JavaScript runs handleClick during rendering, logs immediately, and hands React the return value (undefined) as the "handler." The button then does nothing when clicked. Parentheses mean call now; no parentheses means here is a function to call later. Burn that in.
Inline arrow handlers vs named functions
You have two idioms, and both are correct:
Reach for a named function when the logic is more than a line or two, or you'll reuse it — it keeps JSX readable and stack traces meaningful. Reach for an inline arrow when the handler is trivial, or when you need to pass an argument. That second case is the one people trip on:
The arrow () => deleteItem(id) is a brand-new function that, when called, calls deleteItem with the right id. It's the idiomatic way to bake an argument into a handler.
Passing a handler is like giving a friend your phone number versus phoning them right now. onClick={handleClick} hands React the number — "call me when it happens." onClick={handleClick()} dials immediately and hands React the dead line afterwards. And () => deleteItem(id) is a sticky note that says "when the time comes, ring this specific person" — the argument travels with the instruction.
The SyntheticEvent
When React calls your handler, it passes one argument: an event object. It looks and behaves exactly like a native DOM event — e.target, e.preventDefault(), e.stopPropagation(), e.key — but it is React's own wrapper, called a SyntheticEvent. React wraps the native event so the same properties behave identically across every browser; you write to one API and stop worrying about Firefox versus Safari.
Two methods you'll use constantly: e.preventDefault() cancels the browser's built-in reaction (a form reloading the page, a link navigating away), and e.stopPropagation() stops the event from bubbling up to parent handlers. e.target is the element that fired the event — for inputs, e.target.value is what the user typed.
For years, React's tutorials carried a famous warning: the SyntheticEvent was pooled — reused for performance, its fields wiped after the handler ran — so touching e inside an async callback gave you a blanked-out object, and you had to call e.persist(). React 17 removed event pooling entirely. That optimisation no longer pays off on modern engines, and it caused far more confusion than it saved cycles. If you find a Stack Overflow answer telling you to call e.persist(), it is describing a world that ended in 2020. In React 19 the event object is yours to keep.
The canonical example: a form with onSubmit
The single most important event pattern in React is a form submit. A <form> in HTML reloads the whole page when submitted — a relic from the era of server round-trips. In a React app that reload throws away your entire application state. So you always call e.preventDefault():
Put onSubmit on the <form>, not on the button — that way it fires whether the user clicks the button or presses Enter in a field, which is what people expect. Here we read the value from e.target.elements.username.value (an uncontrolled input — the DOM holds the value). In the next chapter we'll instead keep that value in state; both are valid, and knowing when to use each is a real skill.
Reflexes that separate people who know React events from people who guess:
• Pass, don't call. onClick={fn}, never onClick={fn()} — unless you deliberately want a function returning a handler.
• Every form's submit starts with e.preventDefault(). It's muscle memory; forget it and the page reloads and eats your state.
• Put onSubmit on the form, not the button — Enter-to-submit comes free and accessibility improves.
• The handler's first argument is always the event. Name it e, reach for e.target, e.preventDefault(), e.key without ceremony.
What React does under the hood
You might picture React attaching a listener to every button and input. It doesn't. React attaches a small number of listeners at the root of your app (the container passed to createRoot) and lets native events bubble up to it — a technique called event delegation. When an event reaches the root, React figures out which component it started on and calls the right handlers, simulating the bubbling you'd expect. One listener at the top serves thousands of elements, so adding a hundred onClicks costs almost nothing.
This is elegant trivia that rarely affects you — the SyntheticEvent makes it feel like a normal per-element listener. It matters in exactly two situations: when you mix React with hand-written addEventListener code, and when you need to react during the capture (downward) phase instead of the default bubbling (upward) phase. For that, append Capture to the prop name:
Click the button and the console prints in order 1, 2, 3: capture handlers fire top-down before the event reaches the target, then bubble handlers fire bottom-up. You'll rarely need onClickCapture, but knowing it exists — and that React models both DOM phases faithfully — is a senior-level detail.
The Academy's enrollment form is a plain <form onSubmit={handleSubmit}>. The very first line of the handler is e.preventDefault() — without it, submitting would reload the page mid-signup and lose everything the student typed. From there the handler validates the fields and sends them to a Next.js server action. The lesson quiz uses the argument pattern everywhere: each answer button is onClick={() => selectAnswer(option.id)}, so one handler serves every option while still knowing exactly which one was picked. Two patterns from this chapter carry almost the entire interactive surface of a real React app.
Common Mistakes
- Calling the handler instead of passing it.
onClick={handleClick()}runs during render and passesundefinedto React. Drop the parentheses:onClick={handleClick}. This is the classic React bug. - Forgetting
e.preventDefault()on a form. The page reloads, state resets, and it looks like your handler "didn't run." It ran — the browser's default just clobbered it. - Lowercase or DOM-style prop names. It's
onClick, notonclick;onChange, notonchange. React uses camelCase. A lowercaseonclickis silently ignored (or treated as an unknown attribute). - Passing an argument without an arrow.
onClick={deleteItem(id)}fires immediately. Wrap it:onClick={() => deleteItem(id)}. - Chasing the old pooling ghost. Calling
e.persist()or copying event fields "to be safe" — pooling was removed in React 17. In React 19 the event object stays valid; the extra code is cargo-culting. - Putting submit logic on the button's
onClick. PreferonSubmiton the<form>so keyboard Enter works and screen readers behave. Reserve the button'sonClickfor non-submit actions.
You write onClick={console.log("clicked")} and refresh the page. Before you click anything, "clicked" appears in the console — once — and then the button is dead no matter how many times you click. Why? Because console.log("clicked") is called during render; it runs, prints, and returns undefined, which becomes your "handler." To fix it you need a function React can call later: onClick={() => console.log("clicked")}. This one mistake, in miniature, teaches the entire difference between referencing a function and invoking one — the mental model everything in this chapter rests on.
Interview Corner
onClick={handleClick()}?"onClick prop: onClick={handleClick}. React stores the reference and calls it when the click happens. onClick={handleClick()} is wrong because the parentheses call the function during render, immediately, and pass its return value — usually undefined — to React as the handler, so nothing runs on the actual click."onClick={() => handleClick(id)}, so the call is deferred until the click.e.target, e.preventDefault(), e.stopPropagation(), e.key — but normalises behaviour so I write once and it works in every browser. It used to be pooled and reused for performance, which required e.persist() in async code, but React 17 removed pooling, so in React 19 the event object stays valid after the handler returns."e.nativeEvent when you truly need the raw one.createRoot, lets native events bubble up, then dispatches to the right components — reconstructing capture and bubble phases so onClickCapture and onClick both work. That keeps listener count tiny regardless of tree size. It bites in two places: mixing in manual addEventListener code — since ordering and stopPropagation across the two systems can surprise you — and portals, where DOM position and React-tree position diverge, so events bubble along the React tree, not the DOM tree. Since React 17, delegation attaches to the app root rather than document, which is what lets multiple React versions coexist on one page without stealing each other's events."onClick={fn()} and asked "what happens when this renders?" Talk through call-vs-reference before you fix it.onSubmit on the form beats onClick on the button, keyboard handling with onKeyDown, and clean handler structure.- In the editor above, take the form example and add an
onInputhandler to the input that logse.target.valueon every keystroke. Notice how it fires continuously, unlikeonSubmit. - Break the button on purpose: change
onClick={handleClick}toonClick={handleClick()}, run it, and watch the log fire on render while the click goes dead. Then fix it — and say out loud why it broke. - Build a list of three items where each has a Delete button using
onClick={() => deleteItem(id)}. Confirm the console logs the correct id each time. - Out loud (as in a micro1 screen), explain the difference between passing a function reference and invoking a function, using
onClickas your example — in under thirty seconds.
Chapter Summary
- Attach handlers with camelCase JSX props (
onClick,onChange,onSubmit,onKeyDown) and pass a function reference, not a call. onClick={fn}runs on click;onClick={fn()}runs during render and breaks the button — parentheses mean "call now."- Use an inline arrow to pass arguments:
onClick={() => doThing(id)}. - Handlers receive a SyntheticEvent — a cross-browser wrapper with
e.target,e.preventDefault(),e.stopPropagation(),e.key. Event pooling was removed in React 17; ignore olde.persist()advice. - The canonical form pattern is
<form onSubmit={handler}>withe.preventDefault()as the first line to stop the page reload. - Under the hood React uses event delegation at the app root;
onClickCapturetaps the capture phase. Both rarely matter, but knowing them signals depth.
You can now catch what the user does — every click, keystroke, and submit flows into your handlers. But catching an event and remembering it are different things. Log the input's value all you like; the moment the component re-renders, that value is gone, because a plain variable doesn't survive a render. To make the UI actually change in response to events — to hold a count, a typed name, a toggle — you need memory that React watches. That's the beating heart of every React app, and it's exactly where we go next: useState: Your First State.
Editorial reflection — events are deceptively humble. Almost everyone learns
onClickin an afternoon and assumes there's nothing more to it — and then ships thehandleClick()bug, or the form that reloads the page, or the async handler that fights a ghost that died in 2020. The depth here isn't in the syntax; it's in the one idea underneath all of it — the difference between naming a function and running it. Master that distinction and you haven't just learned React events; you've learned how JavaScript itself hands work to the future. Everything reactive you build from here rests on it.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.