Module 4: Events And Handlers
Module introduction: the interface that responds
Why this module exists here
In module 3 we completed the equation that holds up the whole guide. You learned that a component has state —its own memory, which persists between renders— and that on changing that state with its setter (setQuery, setCount), React runs the component again and updates the screen. That's the live equation: UI = f(state). You change the state, and the interface follows.
But look again at how we executed those examples. In the Node mini runtime we called setCount ourselves, by hand, from the test code: setCount(c => c + 1) written in the script, three times, to simulate three interactions. The screen changed, yes, but because the programmer pushed the setter. The real user —the person who types in the search bar, the one who clicks "Add to cart"— was not in the equation. The link that connects what a person does with the setter that fires was missing. Without that link, the interface only moves if you, from the code, move it; and that's not an interface, it's an animation with a script.
Connection with the module. This module installs that missing link: events. An event is something that happens in the interface by the user's action —a click, a key, the submission of a form—. React lets you connect a function to an event; that function is called an event handler, and when the event occurs, React runs it. Inside the handler is where you finally call the setter. The complete chain, which is the heart of this module, looks like this:
user does something -> React fires the event -> your handler runs ->
handler calls setState -> the state changes -> React re-renders
This is the map lesson: we don't go deep into any piece yet, but we install the idea (events close the interactivity loop), we show it executed end to end, and we give the map of how each lesson builds a part. In this module we work on four things: passing the function and not calling it (the onClick={fn} vs onClick={fn()} trap), the event object (e.target.value, e.preventDefault()), controlled inputs (the SearchBar whose value comes from the state), and passing data up via callbacks (the ProductCard's onAddToCart). What this module does not touch: where the shared state lives —the cart that many pieces touch— is lifting the state, and that's module 7. Here the data goes up via a callback, but the state that receives it we already assume in the parent.
And the promise as always, kept in every lesson: nothing is cited from memory, everything is executed. Browser React can't be "run and see the DOM" here, so we do the honest thing: the real JSX syntax with handlers is shown in the code blocks —it's the exact API you'll write— and the pure logic of an event's flow is executed in Node with just JavaScript. We model a "click" as what it really is inside —the invocation of the function you stored in onClick— and we model the event object as the normal object it is. Each output in the "What to expect" blocks is the literal output of running the code with Node 18.
An analogy: the house doorbell
Imagine the doorbell of a house. There's a button outside, next to the door. And there's an action: inside a ding-dong sounds. The button by itself does nothing; the action by itself never fires. What makes the doorbell useful is the wire that connects the button with the bell: when someone presses the button, the bell rings. You, when you install the doorbell, don't stand there waiting to ring the bell by hand every time a visitor arrives. You do something much better: you connect the button to the bell just once, and from then on anyone who presses fires the sound, without you being present.
An event handler in React is exactly that wire. The button is the DOM element (<button>, <input>). The bell is your handler function (what you want to happen). And onClick={handleClick} is the wire: you tell React "when someone clicks this button, ring this function". You don't stand watching the button to react; you connect the handler once, and React takes care of firing it every time the user clicks.
Two of the module's lessons already come out of this image. The first: when you install the doorbell, you connect the bell, not the sound already rung. It would be absurd to ring the bell at the moment of connecting the wire and expect that to stay "connected" for later. Well then: onClick={handleClick} connects the bell; onClick={handleClick()} rings the bell right there, on install, and leaves the wire connected to nothing. That's lesson 3's trap, and you already intuit it from the doorbell. The second: when the bell rings, sometimes you want to know who rang or how —that information comes with the event—; in React it arrives as the event object, lesson 4. Keep the doorbell image: you connect the button to an action, once, and the action fires on its own when the user acts.
The case that goes with us: Mercado's storefront, now interactive
We continue with the Mercado storefront —the store's frontend: the search bar, the product list, the cart—. Up to module 3 it was a photo that changed only if you, from the code, passed it props or pushed a setter. In this module we connect its events to it, and for the first time it responds to a real user. Two pieces concentrate the whole module:
- The
SearchBar: a controlled input. Its text lives in thequerystate; each key the user types firesonChange, which updatesquery; and the input always shows what the state says. It's the loop lesson 5 is about. - The "Add to cart" button of the
ProductCard: when the user clicks, theProductCardcalls a callback it received via props,onAddToCart(product), and "sends" the product up toApp, which decides to add it to the cart. It's the upward flow of lesson 6.
Remember the data: a product (Product) has id, name, priceCents (the price in cents, as an integer: 2599), category, and inStock. When showing it, we format it with formatPrice(2599) → "$25.99". And remember the storefront tree, because events walk it in both directions:
flowchart TD
App[App] --> SearchBar[SearchBar]
App --> ProductList[ProductList]
App --> Cart[Cart]
ProductList --> PC1[ProductCard]
ProductList --> PC2[ProductCard]
PC1 -. onAddToCart product .-> App
SearchBar -. onChange query .-> App
The solid arrows are the props coming down (the usual thing, module 1). The dotted arrows are the module's novelty: the data going up via callbacks —the query that goes up from the SearchBar, the product that goes up from the ProductCard—. Data goes down via props; data goes up via function calls. That symmetry is half the module.
Worked example: a click that raises the cart counter
Nothing convinces like seeing it happen end to end. We're going to set up the complete chain —user → event → handler → setState → re-render— with the smallest possible piece: a button that, on each click, raises the cart's item counter by one. First, the component as you'll write it in real React. This is real JSX; observe it:
function CartButton() {
const [count, setCount] = useState(0);
const handleClick = () => setCount((c) => c + 1); // the handler
return <button onClick={handleClick}>Cart ({count})</button>;
}
Read it with the doorbell analogy. handleClick is the bell: the function we want to ring. onClick={handleClick} is the wire: it connects the button's click with that function. And inside the bell, setCount(c => c + 1) —the functional updater you learned in module 3— raises the counter. Notice that we pass handleClick to onClick without parentheses: we pass the function, not the result of calling it. That distinction is lesson 3; for now take it as given.
Now, how do we execute it if there's no browser? With the same trick as the whole guide, extended with a new idea. A user's "click", inside, isn't magic: it's React invoking the function you stored in onClick. So we model the click as exactly that —find element.props.onClick and call it— and reuse module 3's state mini runtime (a memory cell that persists between renders):
// Mini runtime: one state cell (as in module 3) + a button with onClick.
// Models the full cycle: click -> handler -> setState -> re-render.
let cell;
let firstRender = true;
function useState(initial) {
if (firstRender) cell = initial;
const setState = (next) => {
cell = typeof next === 'function' ? next(cell) : next;
render(); // changing the state triggers a re-render
};
return [cell, setState];
}
// h(): models what you write in JSX. Stores onClick as a FUNCTION in props.
function h(tag, props, ...children) {
return { tag, props: props || {}, children: children.flat() };
}
// The component: a cart counter with an "Add" button.
function CartButton() {
const [count, setCount] = useState(0);
const handleClick = () => setCount((c) => c + 1); // the handler
console.log(` render -> button shows: "Cart (${count})"`);
return h('button', { onClick: handleClick }, `Cart (${count})`);
}
let tree;
function render() {
tree = CartButton();
firstRender = false;
}
// dispatchClick(): models the user's click. React finds the onClick and INVOKES it.
function dispatchClick(element) {
console.log('CLICK ->');
element.props.onClick(); // pass the function, not call it: here React calls it
}
console.log('=== The interface responds: click -> handler -> setState -> re-render ===\n');
render(); // first render
dispatchClick(tree);
dispatchClick(tree);
dispatchClick(tree);
console.log(`\nFinal state: count = ${cell}`);
What to expect. When you run the file with Node, the output is exactly this:
=== The interface responds: click -> handler -> setState -> re-render ===
render -> button shows: "Cart (0)"
CLICK ->
render -> button shows: "Cart (1)"
CLICK ->
render -> button shows: "Cart (2)"
CLICK ->
render -> button shows: "Cart (3)"
Final state: count = 3
Read the output as the chain of events it is. The first render produced a button that says "Cart (0)" —the initial state—. Then comes the first CLICK ->: dispatchClick found the function stored in onClick and invoked it. That function (handleClick) called setCount(c => c + 1), which raised the cell to 1 and triggered a re-render; that's why, right below the CLICK, "Cart (1)" appears. The second click repeats the cycle and gives "Cart (2)"; the third, "Cart (3)". The final state is 3.
Stop at the essential thing: we never called setCount even once in the test code. The only thing we did was "click" —invoke the onClick—; the setter was called by the handler, not by us. That's the new link. In module 3, to get from 0 to 3 we had to write setCount three times by hand; here, the user "clicks" three times and the handler takes care of the rest. The interface finally responds to an action, not to an instruction written in the script. Compare it with the doorbell: we didn't ring the bell; we pressed the button, and the wire rang the bell on its own.
The module map
Keep this route; it's how each lesson builds a part of the "events" link:
Topic Lesson Key idea
──────────────────────────────── ──────── ──────────────────────────────────────────
Respond to events L2 a handler is a FUNCTION; onClick connects
it; React invokes it when the event occurs
Pass the function, don't call it L3 onClick={fn} connects; onClick={fn()} runs
in the render and leaves onClick undefined
The event object L4 e.target.value (read the input),
e.preventDefault() (stop the browser)
Controlled inputs L5 value={query} + onChange={setQuery};
the state is the single source of truth
Data going up (callbacks) L6 the parent gives onAddToCart via props; the
child invokes it; the data goes up, parent decides
Clean handlers L7 move the heavy logic out of the JSX into a
named function
──────────────────────────────── ──────── ──────────────────────────────────────────
Wire the storefront L8 the mini-project, executed
The boundary: what does NOT enter this module
Knowing the boundary saves you from expecting things that come later.
- Where the shared state lives —lifting the state— is module 7. In this module, when the
ProductCardcallsonAddToCart(product), the data goes up to the parent; but how and where the parent stores that cart, and how it comes back down to a siblingCartcomponent, is lifting the state (module 7). Here we assume the parent already has its state and its handler; we focus on the passing of the data upward. - Deriving instead of storing —the
ProductListfiltered by the search— is module 5. In this module's mini-project you'll see the list filtered by thequerythat goes up from theSearchBar; that filtering is a preview. Why the filtered list is computed from the state and not stored in another state is module 5's thesis. - State itself —
useState, the snapshot, the updater, immutability— was module 3. Here we use it as an already-known tool: the handlers call the setters you learned there. IfsetCount(c => c + 1)sounds like gibberish, go back to module 3. - And everything from the ecosystem that the previous modules already delimited stays the same: HTML/CSS in depth →
web-fundamentals-html-css; Next.js/SSR →nextjs-app-router; global state and server data →frontend-state-and-data; styles/design systems →ui-systems-and-design-implementation.
Common mistakes
Believing that "connecting a handler" is "calling a function". What happens: you write onClick={handleClick()} thinking that's how "the handler runs when clicked". Why it happens: in normal JavaScript, to use a function you call it with parentheses; it's hard to see that here we want to pass it, not use it yet. How to spot it: the handler runs on its own, as soon as the component is painted, without anyone having clicked; and the real click does nothing. How to fix it: remember the doorbell —you connect the bell, you don't ring it on install—. onClick={handleClick} connects; onClick={handleClick()} rings right there. Lesson 3 demonstrates it by executing; for now, keep in mind that you pass onClick the name of the function, without parentheses.
Expecting the state to change without an event (or without a setter). What happens: someone writes count = count + 1 inside a handler, or changes a normal variable, and expects the screen to update. Why it happens: they forget that the UI only follows the state, and that the state only changes with its setter. How to spot it: the handler runs (you see it in a console.log) but the screen doesn't move. How to fix it: inside the handler, call the setter (setCount(...)), don't reassign the variable. The event is who fires; the setter is how the state changes; and the re-render is the consequence. All three have to be there.
Thinking this module also solves where to store the cart. What happens: on seeing onAddToCart(product) send the product up, someone tries, right here, to build the complete cart shared between ProductList and Cart, and gets tangled up with who owns the state. Why it happens: the upward flow and the shared state feel like the same topic. How to spot it: you ask yourself "but where do I put the cart's useState so both see it?" and can't find a comfortable place. How to fix it: separate the two questions. This module answers how the data goes up (via a callback the parent gives via props). Where the state that receives it lives —the common ancestor, App— is module 7. Here it's enough that the parent has a handler that receives the data; the design of the shared state comes later.
Exercises
Exercise 1 — Name the chain. In your own words, write the complete chain of what happens from the moment the user clicks the Cart (0) button until the screen shows Cart (1), naming the five stages (event, handler, setter, state change, re-render). Use the worked example's output as evidence.
See solution
- The user clicks the
<button>. That's the event (click). In the model,dispatchClickrepresents it: it printsCLICK ->. - React invokes the handler connected in
onClick: the functionhandleClick. In the model,element.props.onClick(). - The handler calls the setter:
setCount(c => c + 1). It's the only thing the handler does. - The state changes: the cell goes from
0to1. The setter does it and, right after, triggers the re-render. - React re-renders: it runs
CartButtonagain with the new state, and produces a button that says"Cart (1)". In the output, the linerender -> button shows: "Cart (1)"appears right below theCLICK ->.
The key evidence is that in the test code we never wrote setCount: we only "clicked". The setter was called by the handler. That's the link this module adds to UI = f(state).
Exercise 2 — The button with no wire. Suppose someone writes <button>Cart ({count})</button> without onClick. Explain, with the doorbell analogy, what it's missing, and what would happen on clicking. Then say what changes if onClick={handleClick} is added.
See solution
The button without onClick is missing the wire: there's a button (the presser) and there may be a bell (the handleClick function defined somewhere), but nothing connects them. On clicking, nothing happens: React has no function registered for that event on that element, so it invokes nothing, the state doesn't change, and the screen stays the same. It's like a doorbell with the button in place but not wired to the bell: you press it and there's silence.
On adding onClick={handleClick} you connect the wire: you tell React "when someone clicks this button, invoke handleClick". From then on, each click fires the handler, which calls the setter, which changes the state, which re-renders. The button comes to life. Notice you pass handleClick without parentheses —you connect the bell, you don't ring it on install—.
Exercise 3 — Data going down or data going up? For each storefront situation, say whether the data goes down (via props) or goes up (via a callback), and why: (a) App passes ProductCard the product's name; (b) the user clicks "Add to cart" and the ProductCard tells App which product to add; (c) App passes the SearchBar the search's current text; (d) the user types in the SearchBar and it tells App the new text.
See solution
- (a) Goes down.
Appknows the data (the name) and passes it toProductCardvia a prop (name). It's the usual flow: parent → child, top to bottom. (Module 1.) - (b) Goes up. The click happens in the child, but the one who decides what to do with the cart is the parent. The child can't "reach" the parent directly; what it does is invoke a callback the parent gave it via props (
onAddToCart(product)). The data (the product) travels from the child to the parent via that call. (Module 4, lesson 6.) - (c) Goes down. The search text lives in
App's state;Apppasses it to theSearchBarvia thequeryprop. Parent → child. (Lesson 5.) - (d) Goes up. The typing happens in the
SearchBar(the child), but thequerystate lives inApp(the parent). TheSearchBarinvokes the callbackonQueryChange(newText)thatAppgave it, and the new text goes up. (Lessons 5 and 6.)
The general rule: data goes down via props; data goes up via callback calls. Props are read-only (the child can't change them), so the only way a child has to affect the parent is to ask it by invoking a function the parent lent it.
Summary and next step
In this lesson you installed the link that UI = f(state) was missing: events, and their operational piece, the event handler —a function you connect to an event (onClick={handleClick}) and that React runs when the event occurs—. You saw, with the doorbell analogy, that connecting a handler is running a wire between a button and an action, just once, so the action fires on its own when the user acts. And you measured it by executing the complete chain: a button that, by "clicks", raises the cart counter from 0 to 3 without us calling the setter even once —the handler called it—. With this, the interface finally responds to a user, not to a script.
Before moving on you should be able to: name the five stages of the chain (event → handler → setter → state change → re-render); explain with the doorbell why you pass the function and don't call it; distinguish data that goes down (props) from data that goes up (callback); and locate the boundary —what belongs to this module (connecting and firing events, sending data up) and what belongs to module 7 (where the shared state lives)—.
Lesson 2 takes the first piece of the map and nails it: responding to events with onClick and handlers. You're going to see, executed, what exactly a handler is —a function stored in a prop of the element—, how you verify there's a function there (name included), and how a "click" is nothing more than React invoking that function. There, with running code, "connecting a handler" goes from analogy to mechanic.
Resources
- React, "Responding to Events" — react.dev/learn/responding-to-events. The official page that introduces event handlers,
onClick, and the rule of passing the function without calling it. The starting point of this module. In English. - React, "Adding Interactivity" — react.dev/learn/adding-interactivity. The index of the section that covers events, state, and re-render; this whole module lives here, and it links with module 3. In English.
- React, "Reacting to Input with State" — react.dev/learn/reacting-to-input-with-state. How to think of the UI as a function of the state that responds to the user's input; the mental frame of this module. In English.
- MDN, "Introduction to events" — developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events. What an event is in the browser, what handlers are, and how the event model React wraps works. In English.