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 the query state; each key the user types fires onChange, which updates query; 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, the ProductCard calls a callback it received via props, onAddToCart(product), and "sends" the product up to App, 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 ProductCard calls onAddToCart(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 sibling Cart component, 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 ProductList filtered by the search— is module 5. In this module's mini-project you'll see the list filtered by the query that goes up from the SearchBar; 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. If setCount(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 depthweb-fundamentals-html-css; Next.js/SSRnextjs-app-router; global state and server datafrontend-state-and-data; styles/design systemsui-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
  1. The user clicks the <button>. That's the event (click). In the model, dispatchClick represents it: it prints CLICK ->.
  2. React invokes the handler connected in onClick: the function handleClick. In the model, element.props.onClick().
  3. The handler calls the setter: setCount(c => c + 1). It's the only thing the handler does.
  4. The state changes: the cell goes from 0 to 1. The setter does it and, right after, triggers the re-render.
  5. React re-renders: it runs CartButton again with the new state, and produces a button that says "Cart (1)". In the output, the line render -> button shows: "Cart (1)" appears right below the CLICK ->.

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. App knows the data (the name) and passes it to ProductCard via 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; App passes it to the SearchBar via the query prop. Parent → child. (Lesson 5.)
  • (d) Goes up. The typing happens in the SearchBar (the child), but the query state lives in App (the parent). The SearchBar invokes the callback onQueryChange(newText) that App gave 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