At the close of the previous module we left off with a confession: CicloUrbano's catalogue is a shop window. Data arrives, cards paint themselves with their colour and status badge, but the app doesn't react to anything the person using it does. What's missing is the wire that connects the interface to the logic, and that wire is events. In this lesson you'll learn how React connects a click, a keystroke or a focus change to a function of yours: the exact syntax, the classic mistake of calling the handler instead of passing it, what a synthetic event is and what API it offers, how to pass arguments, how bubbling and capturing work, and how to name handlers so the code is still readable six months from now. By the end, TypeSelector will stop being a mock-up and BikeCard will let its parent know which bike was clicked.

Contents

  1. What an event handler is and where it lives
  2. onClick versus onclick: the real differences
  3. Pass the function, don't call it
  4. The synthetic event object
  5. preventDefault and stopPropagation
  6. Handlers with arguments: inline arrow functions
  7. Bubbling and capturing
  8. Catalogue of common events
  9. Where to define the handler and how to name it
  10. CicloUrbano: a selector that responds and a card that notifies

  1. What an event handler is and where it lives

An event handler is an ordinary function that React runs when something happens in the interface: a click, a keystroke, a form submission. There's nothing special about it: it's a plain JavaScript function declared inside your component's body.

// src/components/TestButton.jsx
function TestButton() {
  // 1. Declare a function inside the component
  function handleClick() {
    console.log('The button was clicked');
  }

  // 2. Hand it to React through a prop on the element
  return <button onClick={handleClick}>Click here</button>;
}

export default TestButton;

Three details worth fixing in your mind from minute one:

  • The function is declared inside the component, not outside. That way it can read that specific instance's props and state, which is exactly what it'll need as soon as it does something useful.
  • onClick is a prop, not an HTML attribute. It's written in camelCase, like className or tabIndex, because JSX uses property names, not attribute names.
  • The value goes inside curly braces, because it's a JavaScript expression: the reference to the function. In quotes it would be a plain string, and React would warn you with an error.

This pattern — declare the function above, reference it below — is what you'll see in 95% of the React code you come across.

  1. onClick versus onclick: the real differences

If you come from framework-free JavaScript you know two ways of listening for a click: the onclick="..." HTML attribute and element.addEventListener('click', fn) in the script. React replaces both with a third.

Aspect HTML onclick="..." DOM addEventListener React onClick={...}
Spelling All lowercase Lowercase name, as a string camelCase: onClick, onMouseEnter
Value A string of code A function reference A function reference
Where it's written In the markup In the JavaScript, looking up the node In the markup, but it's JavaScript
How many listeners One; the second overwrites the first As many as you want One per prop; for several, a function that calls several
Needs removing No Yes, with removeEventListener, or it leaks memory No: React does it on unmount
Variable scope Global (window) The closure where it's defined The component's: props and state at hand
Needs to look up the node No Yes (getElementById, querySelector) No: the handler travels with the element
Event object Native Native Synthetic (a wrapper, see section 4)

Look at the two most important rows. The first is that in React the value is a function, not a string of code: there's no evaluating text, the editor autocompletes, and syntax errors are caught at compile time. The second is that you don't have to remove the listener: when the component disappears from the screen, React cleans up for you. With manual addEventListener, forgetting the matching removeEventListener is one of the most common sources of memory leaks.

One technical nuance worth knowing, even though it doesn't change how you write code: React does not attach a listener to every <button>. It attaches a handful of listeners to the app's root node (the <div id="root"> you saw in lesson 01-03) and, when an event arrives, decides which of your handlers should run. This technique is called event delegation, and it's what lets a thousand buttons on screen cost far fewer than a thousand listeners.

flowchart LR
    A["Click in the browser"] --> B["Single listener<br/>at React's root"]
    B --> C["React identifies the<br/>element in the tree"]
    C --> D["Runs your function<br/>handleClick(event)"]

  1. Pass the function, don't call it

This is the most common mistake among people starting out with events in React, and it earns its own section because the symptom is so misleading.

// CORRECT: we pass the reference to the function. React will call it when it's due
<button onClick={handleClick}>Book</button>

// WRONG: we call it ourselves during render
<button onClick={handleClick()}>Book</button>

What exactly happens in the second line? Remember that JSX is evaluated while rendering. The parentheses run the function right then, and what gets assigned to onClick isn't the function, but its return value. Since handleClick doesn't return anything, onClick ends up being undefined.

The consequences, in order of appearance:

  1. The console.log message shows up the moment the page loads, without anyone having clicked anything.
  2. Clicking the button does absolutely nothing, because onClick is undefined.
  3. If the handler updates state, it triggers a new render, which calls the function again, which updates state again… an infinite loop, and the tab freezes with a "Too many re-renders" error.

The mnemonic rule is simple: with parentheses, now; without parentheses, later.

onClick={handleClick}        // later (when clicked)  ✔
onClick={handleClick()}      // now (during render)    ✘
onClick={() => handleClick()}  // later, wrapped in another function  ✔

The third form is valid, and you'll need it in section 6, once you have to pass arguments. But if the handler takes no arguments, write the first form: it's shorter and more direct.

  1. The synthetic event object

When React runs your handler, it passes it one argument: the event object.

function handleClick(event) {
  console.log(event.type);            // "click"
  console.log(event.target);          // the DOM node that received the click
  console.log(event.currentTarget);   // the node where the handler is attached
}

It isn't the browser's native event: it's a synthetic event (SyntheticEvent), an object React builds by wrapping the native one.

Why it exists

In the years React was born, every browser exposed events in slightly different ways: different property names, incompatible behaviour, a preventDefault that some browsers called something else entirely. The synthetic event solves three things:

Reason What it delivers
Normalization The same API across every browser; your code carries no browser-specific branches
Compatibility with delegation React can reconstruct the event's path through the component tree, which doesn't always match the DOM tree
Stable interface React guarantees which properties exist, no matter how the browser changes

The synthetic API mirrors the native one, so everything you already know about DOM events still holds. The properties and methods you'll use daily:

Member What it is
event.target The element where the event originated (the deepest one)
event.currentTarget The element the running handler is attached to
event.type The event type as a string: "click", "submit", "keydown"
event.preventDefault() Cancels the browser's default action
event.stopPropagation() Stops the event from bubbling further up the tree
event.key On keyboard events, the key that was pressed: "Enter", "Escape", "a"
event.nativeEvent The original native event, in case you need something the synthetic one doesn't expose

The distinction between target and currentTarget matters. If a <button> contains a <span> and you click on the text, event.target will be the <span>, while event.currentTarget will be the <button>. Whenever you want to identify "the element I attached the handler to", always use currentTarget.

function IconButton() {
  function handleClick(event) {
    console.log(event.target.tagName);        // "SPAN" if you click the text
    console.log(event.currentTarget.tagName); // "BUTTON" always
  }

  return (
    <button onClick={handleClick}>
      <span>Book</span>
    </button>
  );
}

A useful bit of history: up to React 16, synthetic events were pooled and reused: as soon as the handler finished, its properties were wiped, and reading them later required calling event.persist(). That disappeared as of React 17. If you find event.persist() in a tutorial, it's old code: in React 19 it isn't needed and does nothing.

  1. preventDefault and stopPropagation

These are two distinct methods that get confused constantly. They do things that have nothing to do with each other.

preventDefault(): cancelling what the browser would do

Some elements have a native behaviour attached: an <a href> navigates, a <form> submits and reloads the page, an <input type="checkbox"> toggles. preventDefault() cancels that behaviour.

function StationLink() {
  function handleClick(event) {
    event.preventDefault();   // the browser does NOT navigate to /stations
    console.log('Show the detail without changing page');
  }

  return (
    <a href="/stations" onClick={handleClick}>
      View stations
    </a>
  );
}

There's an important nuance in React: returning false from the handler cancels nothing. In classic HTML, onclick="return false" did the trick. In React the return value is ignored entirely: you have to call event.preventDefault() explicitly.

Its most frequent use, by far, is submitting forms, which you'll see in depth in Forms and Controlled Components.

stopPropagation(): cutting off the event's climb

Stops the event from bubbling further up towards ancestor elements. It's the tool for the "I have a button inside a clickable card and I don't want clicking the button to also select the card" case.

function CardWithButton() {
  function handleCardClick() {
    console.log('The card was selected');
  }

  function handleButtonClick(event) {
    event.stopPropagation();   // without this, handleCardClick would also fire
    console.log('The book button was clicked');
  }

  return (
    <article onClick={handleCardClick}>
      <h3>Classic Urban</h3>
      <button onClick={handleButtonClick}>Book</button>
    </article>
  );
}
Method What it cancels Typical example
preventDefault() The browser's default action A <form> reloading the page
stopPropagation() The event's climb to its ancestors A button inside a clickable card

The two are independent: you can call one, the other, both, or neither.

  1. Handlers with arguments: inline arrow functions

Very often the handler needs to know what was acted on: which bike type, which id, which row. And there it collides with section 3's rule: you can't write onClick={selectType('urbana')}, because that would run it during render.

The idiomatic solution is to wrap it in an inline arrow function:

<button onClick={() => selectType('urbana')}>Urban</button>

Step by step, here's what happens:

  1. () => selectType('urbana') creates a new function that doesn't run yet.
  2. That function is what gets assigned to onClick.
  3. When someone clicks, React calls that function, and only then does selectType('urbana') run.

If you also need the event object, declare it as the arrow's parameter and pass it along:

<button onClick={(event) => selectType('urbana', event)}>Urban</button>

The alternative: a function that returns a function

There's an equivalent, slightly more sophisticated pattern, called currying:

function TypeButtons() {
  // Takes the type and RETURNS the handler, already prepared
  const onSelectType = (type) => (event) => {
    console.log('Type chosen:', type, 'from', event.currentTarget.textContent);
  };

  return (
    <>
      <button onClick={onSelectType('urbana')}>Urban</button>
      <button onClick={onSelectType('electrica')}>Electric</button>
    </>
  );
}

Here onSelectType('urbana') does run during render, but that's fine: what it returns is another function, and that's what gets assigned to onClick. It's correct, though for beginners the inline arrow is easier to read.

The cost of creating the function on every render

Every time the component renders, the expression () => selectType('urbana') produces a new function. It's a different object from the one before, even though it does exactly the same thing.

Is that a problem? In the vast majority of cases, no. Creating a function in JavaScript is an extremely cheap operation, and the readability you gain more than makes up for it. It only starts to matter when that function is passed as a prop to a memoized component, because then the new reference breaks the memoization and causes unnecessary renders.

That scenario, how to measure it and how to fix it with useCallback, is material for Module 8, specifically The useMemo and useCallback Hooks. For now, keep the practical rule: write inline arrows without fear, and only optimize once you measure a real problem.

  1. Bubbling and capturing

When you click a nested element, the event doesn't only affect that element: it travels through the tree in two phases.

  1. Capture phase: the event descends from the root down to the clicked element.
  2. Bubble phase (bubbling): the event ascends from the clicked element back up to the root.

React lets you listen in both phases. By default, onClick listens during bubbling. Adding the Capture suffix — onClickCapture, onFocusCapture, onKeyDownCapture — makes you listen during capture.

flowchart TD
    subgraph CAPTURE["Phase 1: capture (outside to inside)"]
        direction TB
        C1["main<br/>onClickCapture"] --> C2["article.card<br/>onClickCapture"] --> C3["button<br/>onClickCapture"]
    end
    C3 --> OBJ["Clicked element:<br/>button"]
    OBJ --> B1
    subgraph BUBBLING["Phase 2: bubbling (inside to outside)"]
        direction TB
        B1["button<br/>onClick"] --> B2["article.card<br/>onClick"] --> B3["main<br/>onClick"]
    end

Let's see it with an example that prints the full path:

function PropagationDemo() {
  return (
    <main
      onClickCapture={() => console.log('1. main - capture')}
      onClick={() => console.log('6. main - bubble')}
    >
      <article
        onClickCapture={() => console.log('2. article - capture')}
        onClick={() => console.log('5. article - bubble')}
      >
        <button
          onClickCapture={() => console.log('3. button - capture')}
          onClick={() => console.log('4. button - bubble')}
        >
          Click
        </button>
      </article>
    </main>
  );
}

Clicking the button, the console prints all six messages in exactly that order: 1, 2, 3, 4, 5, 6.

What each phase is for

Phase When it's used
Bubbling (onClick) 99% of cases. It's the natural behaviour you expect
Bubbling on the parent A single handler on the container for many children, identifying the source with event.target
Capture (onClickCapture) Intercepting before the children: analytics logging, closing a dropdown menu, blocking interaction in an area

One important detail: stopPropagation() cuts the path from wherever it's called. If you call it in article's capture phase, the button won't even get to receive its own click. Use it carefully: cutting propagation can break functionality in other components that relied on receiving that event.

  1. Catalogue of common events

React supports practically every DOM event, always in camelCase. These are the ones you'll use constantly:

Prop When it fires Practical notes
onClick On click (mouse, or Enter/Space on a <button>) The most used. On a real <button> it also works with the keyboard
onChange When a field's value changes In React it fires on every keystroke, not on blur like the native DOM
onInput On every text input In React it's almost identical to onChange; use onChange by convention
onSubmit When a <form> is submitted Goes on the form, not the button. Almost always paired with preventDefault()
onFocus When a field receives focus Useful for highlighting or showing contextual help
onBlur When focus is lost The classic moment to validate a field (you'll see it in 03-05)
onKeyDown When a key is pressed Read with event.key. It's what lets you react to Enter or Escape
onKeyUp When a key is released Less common; for shortcuts, prefer onKeyDown
onMouseEnter When the mouse enters the element Doesn't bubble: ideal for single-element effects
onMouseLeave When the mouse leaves The natural pair of the previous one
onDoubleClick On double-click Watch accessibility: there's no keyboard equivalent
onContextMenu When the context menu opens Requires preventDefault() to replace it
onScroll When a container is scrolled Fires a lot; it's worth throttling its frequency

Two warnings about this table:

  • React's onChange isn't the DOM's change. The native one only fires when the field loses focus after having changed; React's fires on every keystroke. This difference is what makes 03-04's controlled components possible.
  • onMouseEnter and onMouseLeave don't bubble, unlike onMouseOver and onMouseOut. That's why they're recommended for visual effects: they don't fire when passing over the children.

  1. Where to define the handler and how to name it

With three handlers the code is still readable; with fifteen, naming discipline is the difference between a maintainable component and a puzzle.

The course's convention

Case Convention Example
Function defined inside the component handle prefix + what happens handleClick, handleSubmit, handleTypeChange
Function prop a component receives on prefix onSelect, onBook, onTypeChange
Handler for a DOM element The native prop, don't invent one onClick, onSubmit, onKeyDown

The underlying idea is symmetry: the child declares what can happen to it (onSelect) and the parent decides what to do when it happens (handleSelection).

// The parent defines the function and passes it down
function Catalogue() {
  function handleSelection(bike) {
    console.log('Bike selected:', bike.model);
  }

  return <BikeCard bike={firstBike} onSelect={handleSelection} />;
}

// The child only knows it must notify; it doesn't know what happens next
function BikeCard({ bike, onSelect }) {
  return <article onClick={() => onSelect(bike)}>…</article>;
}

This pattern is called lifting the handler, and it's the basis of child-to-parent communication in React: props carry data down, function props carry notifications up.

When to define the function outside the JSX

Situation Recommendation
The handler is one line and takes no arguments Named function above: onClick={handleClick}
The handler needs an argument from context Inline arrow: onClick={() => handleClick(type)}
The handler is more than two or three lines Always a named function above: JSX should read like markup
The handler doesn't depend on the component Helper function in src/utils/, imported

The rule that sums it all up: JSX describes the interface, not the logic. If an onClick contains an if, a try, or three statements, that logic belongs in a named function.

  1. CicloUrbano: a selector that responds and a card that notifies

Let's apply all of this to the app. Two concrete changes.

TypeSelector with real handlers

The 02-04 version already used onClick, but with the logic written directly in the JSX. Now that we know how to name and organize handlers, it looks like this:

// src/components/TypeSelector.jsx
import { useState } from 'react';
import { cx } from '../utils/classNames.js';
import styles from './TypeSelector.module.css';

const TYPES = ['todos', 'urbana', 'electrica', 'carga'];

const LABELS = {
  todos: 'All',
  urbana: 'Urban',
  electrica: 'Electric',
  carga: 'Cargo'
};

/**
 * Bike type selector.
 * Props:
 *  - onTypeChange (function, optional): receives the chosen type when it changes
 *
 * Still keeps the type in its own state; in 04-01 that state moves up to the parent.
 */
function TypeSelector({ onTypeChange }) {
  const [chosenType, setChosenType] = useState('todos');

  function handleTypeSelect(type) {
    setChosenType(type);
    if (onTypeChange) {
      onTypeChange(type);
    }
  }

  function handleClearKey(event) {
    if (event.key === 'Escape') {
      handleTypeSelect('todos');
    }
  }

  return (
    <div className={styles.selector} onKeyDown={handleClearKey}>
      <p className={styles.title}>Filter by type:</p>

      {TYPES.map((type) => (
        <button
          key={type}
          type="button"
          className={cx(styles.button, type === chosenType && styles.active)}
          onClick={() => handleTypeSelect(type)}
        >
          {LABELS[type]}
        </button>
      ))}

      <p className={styles.selection}>
        Current selection: <strong>{LABELS[chosenType]}</strong>
      </p>
    </div>
  );
}

export default TypeSelector;

What changed and why:

  • handleTypeSelect is a named function. Before, the logic lived inside onClick; now it does two things (saving the state and notifying the parent), and that no longer fits comfortably in JSX.
  • onClick={() => handleTypeSelect(type)}: an inline arrow because we need to pass each button's type. Without it, the function would run during render, as you saw in section 3.
  • onTypeChange is an optional function prop. If the parent passes it, the selector notifies it; if not, the component keeps working. This is the groundwork for 04-01, where the state will move up to the parent and the filter will actually work.
  • type="button" on the buttons. Without it, a <button> inside a <form> defaults to type="submit" and would submit the form when clicked. It's a cheap precaution that avoids a baffling bug.
  • onKeyDown on the container: thanks to bubbling, a single handler covers all four buttons. Pressing Escape while any of them is focused resets the filter to "All".

BikeCard notifies which bike was clicked

// src/components/BikeCard.jsx
import { cx } from '../utils/classNames.js';
import styles from './BikeCard.module.css';
import StatusBadge from './StatusBadge.jsx';

/**
 * Card for a bike in CicloUrbano's catalogue.
 * Props:
 *  - bike (object, required) { id, model, type, status, stationId, pricePerHour }
 *  - stationName (string, optional, defaults to 'Unknown station')
 *  - onSelect (function, optional): receives the bike object when the card is clicked
 *  - onBook (function, optional): receives the bike object when "Book" is clicked
 */
function BikeCard({
  bike,
  stationName = 'Unknown station',
  onSelect,
  onBook
}) {
  const formattedPrice = bike.pricePerHour.toFixed(2);
  const unavailable = bike.status !== 'disponible';

  function handleCardClick() {
    if (onSelect) {
      onSelect(bike);
    }
  }

  function handleBookClick(event) {
    // Without this, the click would bubble up to the <article> and select the card too
    event.stopPropagation();
    if (onBook) {
      onBook(bike);
    }
  }

  return (
    <article
      className={cx(styles.card, styles[bike.type], unavailable && styles.dimmed)}
      onClick={handleCardClick}
    >
      <h3 className={styles.title}>
        {bike.model} <StatusBadge status={bike.status} />
      </h3>
      <p className={styles.meta}>Type: {bike.type}</p>
      <p className={styles.meta}>Station: {stationName}</p>
      <p className={styles.price}>€{formattedPrice} / hour</p>

      <button type="button" className={styles.action} onClick={handleBookClick}>
        Book
      </button>
    </article>
  );
}

export default BikeCard;

The two key points:

  • onSelect and onBook are optional props. The if (onSelect) check avoids the "onSelect is not a function" error when the parent doesn't pass them. An equally valid alternative is giving a default value: onSelect = () => {}.
  • event.stopPropagation() on the button is essential: the button sits inside the <article>, which already listens for clicks, so without cutting the propagation every "Book" click would also trigger the card's selection. This is the canonical use case from section 5.

A note we'll come back to in Accessibility in Interactive Components: putting onClick on an <article> makes it clickable with the mouse, but not with the keyboard. It's a conscious debt we'll settle before closing the module.

App receives the notifications

// src/App.jsx
import { bikes } from './data/domain.js';
import Header from './components/Header.jsx';
import FleetSummary from './components/FleetSummary.jsx';
import TypeSelector from './components/TypeSelector.jsx';
import BikeList from './components/BikeList.jsx';
import Footer from './components/Footer.jsx';

function App() {
  function handleTypeChange(type) {
    console.log('Type chosen in the selector:', type);
  }

  function handleBikeSelect(bike) {
    console.log('Selected:', bike.id, bike.model);
  }

  function handleBooking(bike) {
    console.log('Book:', bike.id, 'at', bike.pricePerHour, '€/hour');
  }

  return (
    <>
      <Header />
      <main>
        <FleetSummary fleet={bikes} />
        <TypeSelector onTypeChange={handleTypeChange} />
        <BikeList
          first={bikes[0]}
          second={bikes[1]}
          third={bikes[2]}
          onSelect={handleBikeSelect}
          onBook={handleBooking}
        />
      </main>
      <Footer />
    </>
  );
}

export default App;

BikeList just forwards the two function props to each card. For now App only does console.log: actually storing the selected bike requires the state to live in the parent, and that's exactly the subject of Lifting State Up. What matters here is that the flow already works: the click happens on the card and the information reaches the top.

flowchart TD
    APP["App<br/>handleBikeSelect"] -- "onSelect (function prop)" --> LIS["BikeList"]
    LIS -- "onSelect" --> TAR["BikeCard"]
    TAR -. "notifies on click: onSelect(bike)" .-> APP

Open the browser console, click a card and then its "Book" button: you'll see a different message each time, without them mixing up. CicloUrbano now responds.

Common Mistakes and Tips

  • Calling the handler instead of passing it. onClick={handleClick()} runs it during render. If it also updates state, you get an infinite loop and the "Too many re-renders" error. No parentheses, or wrap it in an arrow.
  • Writing the event name in lowercase. onclick isn't a React prop: React treats it as an unknown attribute and warns in the console, suggesting onClick. Always camelCase.
  • Passing the handler as a string. onClick="handleClick()" is HTML syntax; in JSX it assigns a string of text and doesn't work.
  • Expecting return false to cancel the default behaviour. In React the return value is ignored: use event.preventDefault().
  • Confusing preventDefault with stopPropagation. The first cancels what the browser would do; the second cancels the event's climb. They aren't interchangeable.
  • Overusing stopPropagation(). Cutting propagation "just in case" breaks functionality that relies on bubbling, like closing a menu when clicking outside it. Use it only when you have a real conflict.
  • Confusing target with currentTarget. If the clickable element contains an icon or a <span>, target will be that child. To identify the handler's own element, use currentTarget.
  • Forgetting type="button" on buttons inside a form. The default is submit, so the button will submit the form and the page will reload.
  • Using event.persist(). That's React 16-era code. Since React 17 events are no longer pooled and that call is unnecessary.
  • Putting onClick on a <div> or an <article>. It works with the mouse and fails with the keyboard. Use a <button> when you can; if you can't, you'll need to add a role, tabIndex and a keyboard handler, as you'll see in 03-06.
  • Tip: one handler, one responsibility. If handleClick saves state, notifies the parent, and logs analytics, extract helper functions.
  • Tip: make the most of bubbling. A single onKeyDown on the container covers all its children. Less code and fewer chances of missing a case.
  • Tip: check with console.log(event) when something doesn't add up. The synthetic object is inspectable and usually reveals right away whether the problem is the wrong element or the wrong event.

Exercises

Exercise 1

Analyse this component and identify three event-related bugs. Explain the symptom of each one and write the fixed version.

function StationButton({ station }) {
  function handleClick(event) {
    console.log('Station:', station.name);
    return false;   // so the link doesn't navigate
  }

  return (
    <div>
      <a href="/stations" onclick={handleClick()}>
        {station.name}
      </a>
    </div>
  );
}

Exercise 2

Create the BikeRow component (src/components/BikeRow.jsx) as a compact version of the card, meant for a table. It should:

  • Receive bike and two function props: onSelect and onMarkMaintenance.
  • Render a <tr> row with the model, type, status and price, plus a cell with a "Send to workshop" button.
  • Notify with onSelect(bike) when any part of the row is clicked.
  • Notify with onMarkMaintenance(bike) when the button is clicked, without also triggering the row's selection.
  • Log to the console, using event.currentTarget.tagName, which element each handler ran from, to check the difference from target.

Exercise 3

Add fuller keyboard behaviour to TypeSelector, using a single handler on the container:

  • Escape resets to type 'todos' (already done).
  • The ArrowRight key advances to the next type in the TYPES constant, and ArrowLeft goes back, both circularly (from the last type back to the first and vice versa).
  • Any other key should do nothing.

Write only the handler, and explain why it's worth computing the new index from TYPES.indexOf(chosenType).

Solutions

Solution 1.

The three bugs:

Bug Symptom
Lowercase onclick React doesn't recognize the prop; the handler never runs, and a console warning suggests onClick
handleClick() with parentheses The function runs during render: the message appears on page load and onClick ends up undefined
return false In React the return value is ignored: the link navigates anyway and the page reloads. You have to call event.preventDefault()
// src/components/StationButton.jsx

/**
 * Link to a station's detail page.
 * Props:
 *  - station (object, required) { id, name, district, docks }
 */
function StationButton({ station }) {
  function handleClick(event) {
    event.preventDefault();   // cancels the browser's navigation
    console.log('Station:', station.name);
  }

  return (
    <div>
      <a href="/stations" onClick={handleClick}>
        {station.name}
      </a>
    </div>
  );
}

export default StationButton;

Solution 2.

// src/components/BikeRow.jsx
import StatusBadge from './StatusBadge.jsx';

/**
 * Compact row for a bike, used in table views.
 * Props:
 *  - bike (object, required)
 *  - onSelect (function, optional): receives the bike when the row is clicked
 *  - onMarkMaintenance (function, optional): receives the bike when the button is clicked
 */
function BikeRow({ bike, onSelect, onMarkMaintenance }) {
  const formattedPrice = bike.pricePerHour.toFixed(2);

  function handleRowClick(event) {
    console.log('Handler on:', event.currentTarget.tagName); // always "TR"
    console.log('Actual source:', event.target.tagName);     // "TD", "SPAN"…
    if (onSelect) {
      onSelect(bike);
    }
  }

  function handleWorkshopClick(event) {
    event.stopPropagation();   // stops the click from reaching the <tr>
    console.log('Handler on:', event.currentTarget.tagName); // "BUTTON"
    if (onMarkMaintenance) {
      onMarkMaintenance(bike);
    }
  }

  return (
    <tr onClick={handleRowClick}>
      <td>{bike.model}</td>
      <td>{bike.type}</td>
      <td>
        <StatusBadge status={bike.status} />
      </td>
      <td>€{formattedPrice}/h</td>
      <td>
        <button type="button" onClick={handleWorkshopClick}>
          Send to workshop
        </button>
      </td>
    </tr>
  );
}

export default BikeRow;

Clicking the text of a cell, currentTarget is always TR (where the handler lives), while target is TD. Clicking the button, stopPropagation() stops handleRowClick from running: only the button's message appears in the console.

Solution 3.

function handleKeyDown(event) {
  const currentIndex = TYPES.indexOf(chosenType);

  if (event.key === 'Escape') {
    handleTypeSelect('todos');
    return;
  }

  if (event.key === 'ArrowRight') {
    const next = (currentIndex + 1) % TYPES.length;
    handleTypeSelect(TYPES[next]);
    return;
  }

  if (event.key === 'ArrowLeft') {
    // Adding TYPES.length before the modulo avoids negative indexes
    const previous = (currentIndex - 1 + TYPES.length) % TYPES.length;
    handleTypeSelect(TYPES[previous]);
  }

  // Any other key: do nothing, and let the event carry on
}

Computing the index from TYPES.indexOf(chosenType), rather than keeping a separate index state, avoids having two sources of truth that could drift apart: the index is a derived value from the state, exactly as you learned in lesson 02-04. And the modulo operator % gives circular behaviour with no extra if; adding TYPES.length before the modulo is necessary because in JavaScript -1 % 4 is -1, not 3.

Conclusion

Events are the point where the interface stops being a picture and becomes an application. You now know that a handler is an ordinary function declared inside the component and handed to React through a camelCase prop; that the prop receives the function, not its call, and why one extra pair of parentheses can cause anything from a premature message to an infinite render loop; that React hands you a synthetic event that normalizes the API across browsers, with target, currentTarget, preventDefault() and stopPropagation() as everyday tools; that passing arguments means wrapping the handler in an inline arrow, whose cost will only matter once you reach Module 8; and that the event travels through the tree in two phases, with onClick listening during bubbling and onClickCapture during capture.

In CicloUrbano that has become two pieces that now respond: a TypeSelector with named handlers, buttons that highlight the current selection, and keyboard shortcuts; and a BikeCard that accepts the onSelect and onBook function props to notify upward of what happens inside it, with stopPropagation() exactly where it was needed. Data flows down through props; notifications flow up through function props. That two-way flow is React's data flow, and it won't change for the rest of the course.

The app reacts now, but it still always paints the same thing. A bike under maintenance shouldn't offer a "Book" button, a catalogue with no results should say so instead of staying blank, and a notice should only appear when there's something to notify. Painting one thing or another depending on state or props is the next step, and it has more subtlety than it looks — including a trap with the && operator that already peeked through in the JSX lesson. You'll see it in Conditional Rendering.

React Course

Module 1: Getting Started with React

Module 2: React Components

Module 3: Working with Events

Module 4: Advanced Component Concepts

Module 5: React Hooks

Module 6: Routing in React

Module 7: State Management

Module 8: Performance Optimization

Module 9: Testing React Applications

Module 10: Advanced Topics

Module 11: Project: Building a Complete Application

© Copyright 2026. All rights reserved