# Event Handling

Course: [React](https://www.devtutorials.ai/learn/react) · Section: Dynamic User Interfaces
Source: https://www.devtutorials.ai/learn/react/dynamic-ui/event-handling

> Events and event handlers, synthetic events and the event object, click, mouse and keyboard events, passing values to handlers, typing handlers, preventDefault and event propagation.

## Introduction to Events

An **event** is something that happens on the page, such as a click, a key press or a mouse move. **Event handling** means running your own code when an event happens.

In React, you add an event to an element with a prop like `onClick`. You give it a function. React calls that function when the event happens. This function is called an **event handler**.

<figure className="react-fig ngc-fig">
<svg viewBox="0 0 560 120" role="img" aria-labelledby="fig-ev-intro" style={{ width: "100%", maxWidth: 600, height: "auto" }}>
  <title id="fig-ev-intro">Animation: the user clicks a button, React calls the onClick handler, and the handler updates the page</title>
  <defs><marker id="ev1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--ngc-slate)" }} /></marker></defs>
  <rect x="10" y="35" width="140" height="46" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="80" y="54" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>Add to cart</text><text x="80" y="70" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontSize: 11, fontWeight: 400, fontFamily: "var(--sans)" }}>&lt;button&gt;</text><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.05;0.09;0.93;1" dur="8s" repeatCount="indefinite" /><rect x="4" y="29" width="152" height="58" rx="11" style={{ fill: "none", stroke: "var(--ngc-orange)", strokeWidth: 2.5 }} /><text x="80" y="108" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>1. user clicks</text></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.25;0.29;0.93;1" dur="8s" repeatCount="indefinite" /><line x1="155" y1="58" x2="200" y2="58" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} markerEnd="url(#ev1)" /><text x="178" y="28" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>onClick</text></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.3;0.34;0.93;1" dur="8s" repeatCount="indefinite" /><rect x="205" y="35" width="170" height="46" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} /><text x="290" y="54" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>event handler</text><text x="290" y="70" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 11, fontWeight: 400, fontFamily: "var(--mono)" }}>handleClick()</text><text x="290" y="108" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>2. React calls it</text></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.5;0.54;0.93;1" dur="8s" repeatCount="indefinite" /><line x1="378" y1="58" x2="420" y2="58" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} markerEnd="url(#ev1)" /></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.55;0.59;0.93;1" dur="8s" repeatCount="indefinite" /><rect x="425" y="35" width="125" height="46" rx="8" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }} /><text x="488" y="54" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>Cart (1)</text><text x="488" y="70" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 11, fontWeight: 400, fontFamily: "var(--sans)" }}>page updates</text><text x="488" y="108" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>3. state changes</text></g>
</svg>
<figcaption>An event happens, React calls your handler, and the handler updates the page.</figcaption>
</figure>

### Real-World Example

In a bookshop app, each book card has an **Add to cart** button with `onClick={() => addToCart(book.id)}`. A search box has `onChange` and reads `e.target.value` on every key press.

React calls these functions only when the user clicks or types. Each function updates state, and the page shows the new cart count and the new search results.

```tsx
<button onClick={handleClick}>Add to cart</button>
```

Here, `onClick` is the event, and `handleClick` is the event handler.

Event names in React use camelCase: `onClick`, `onChange`, `onKeyDown`. You pass a function, not a string.

## Synthetic Events and Event Object

When an event happens, React calls your handler and gives it one input. This input is called the **event object**. It holds information about what happened.

Most people name it `e` or `event`:

```tsx
function handleClick(e) {
  console.log(e.type); // "click"
}
```

React's event object is called a **synthetic event**. It is React's own version of the browser's event. It works the same way in every browser, so you do not have to worry about browser differences.

<figure className="react-fig ngc-fig">
<svg viewBox="0 0 560 160" role="img" aria-labelledby="fig-ev-synthetic" style={{ width: "100%", maxWidth: 600, height: "auto" }}>
  <title id="fig-ev-synthetic">Animation: each browser sends its own native event; React wraps it in one synthetic event object with the same fields everywhere</title>
  <defs><marker id="ev2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--ngc-purple)" }} /></marker></defs>
  <rect x="10" y="15" width="150" height="34" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="85" y="36" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>Chrome click</text><rect x="10" y="58" width="150" height="34" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="85" y="79" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>Firefox click</text><rect x="10" y="101" width="150" height="34" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><text x="85" y="122" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>Safari click</text><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.15;0.19;0.93;1" dur="8s" repeatCount="indefinite" /><line x1="162" y1="32" x2="238" y2="72" style={{ stroke: "var(--ngc-purple)", strokeWidth: 2 }} markerEnd="url(#ev2)" /><line x1="162" y1="75" x2="238" y2="75" style={{ stroke: "var(--ngc-purple)", strokeWidth: 2 }} markerEnd="url(#ev2)" /><line x1="162" y1="118" x2="238" y2="78" style={{ stroke: "var(--ngc-purple)", strokeWidth: 2 }} markerEnd="url(#ev2)" /><text x="200" y="148" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>React wraps it</text></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.35;0.39;0.93;1" dur="8s" repeatCount="indefinite" /><rect x="245" y="15" width="305" height="120" rx="10" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} /><text x="397" y="38" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>e (synthetic event)</text></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.45;0.49;0.93;1" dur="8s" repeatCount="indefinite" /><text x="262" y="64" textAnchor="start" style={{ fill: "var(--ngc-blue)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>e.type → "click"</text></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.53;0.57;0.93;1" dur="8s" repeatCount="indefinite" /><text x="262" y="88" textAnchor="start" style={{ fill: "var(--ngc-blue)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>e.target → the &lt;button&gt;</text></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.61;0.65;0.93;1" dur="8s" repeatCount="indefinite" /><text x="262" y="112" textAnchor="start" style={{ fill: "var(--ngc-blue)", fontSize: 12, fontWeight: 400, fontFamily: "var(--mono)" }}>e.preventDefault()</text></g>
</svg>
<figcaption>React gives your handler the same event object in every browser.</figcaption>
</figure>

Useful parts of the event object:

- **`e.type`** — the kind of event, such as `"click"` or `"keydown"`.
- **`e.target`** — the element where the event started.
- **`e.currentTarget`** — the element that has the handler.
- **`e.preventDefault()`** — stops the browser's normal action.
- **`e.stopPropagation()`** — stops the event from going up to parent elements.

Click the button and look at the console. The event object tells you the event type, which button was clicked, and where the mouse was.

`e: any` means "any type". Later in this lesson, you will learn the correct type to use.

## Click Events

`onClick` runs a function when an element is clicked. You can write the handler in two ways.

      <h3>Atomic Habits</h3>
      <p>Likes: {likes}</p>
      <button onClick={handleLike}>Like</button>{" "}
      <button onClick={() => setLikes(0)}>Clear</button>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

- **Way 1:** `onClick={handleLike}` gives React the function `handleLike`. Use this when the handler has more than one line.
- **Way 2:** `onClick={() => setLikes(0)}` writes a short function right there. Use this for one short line.

Do not write `onClick={handleLike()}`. The `()` runs the function at once, while the page is drawn, not when you click.

## Mouse Events

Mouse events run when the mouse moves over an element or clicks it. These are the most common ones:

<div className="tscroll">

| Event | When it runs |
|---|---|
| `onClick` | The element is clicked |
| `onDoubleClick` | The element is clicked twice quickly |
| `onMouseEnter` | The mouse moves onto the element |
| `onMouseLeave` | The mouse moves off the element |
| `onMouseMove` | The mouse moves while over the element |

</div>

      <div
        className="cover"
        onMouseEnter={() => setMessage("Mouse is on the box")}
        onMouseLeave={() => setMessage("Mouse left the box")}
        onDoubleClick={() => setMessage("You double-clicked!")}
        onMouseMove={(e) => setPosition({ x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY })}
      >
        Wings of Fire
      </div>
      <p>{message}</p>
      <p>Mouse inside the box: x = {position.x}, y = {position.y}</p>
    </div>
  );
}
`,
    "App.css": `.cover {
  width: 200px;
  height: 90px;
  display: grid;
  place-items: center;
  background: #eff6ff;
  border: 2px solid #0047ba;
  border-radius: 8px;
  font-weight: 600;
  user-select: none;
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Move the mouse onto the box, move it around, double-click it, then move it away. Each action runs a different handler.

## Keyboard Events

Keyboard events run when a key is pressed in an element that has focus, such as an input box.

- **`onKeyDown`** runs when a key is pressed down.
- **`onKeyUp`** runs when the key is let go.

The event object has **`e.key`**. It holds the name of the key, such as `"a"`, `"Enter"` or `"Escape"`.

      <input
        placeholder="Type a book name, press Enter"
        value={text}
        onChange={(e) => setText(e.target.value)}
        onKeyDown={handleKeyDown}
      />
      <p>Last key: {lastKey}</p>
      <ul>
        {books.map((book, i) => <li key={i}>{book}</li>)}
      </ul>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Click in the box and type. Look at this line in `handleKeyDown`:

```tsx
if (e.key === "Enter" && text !== "") {
```

It means: if the key is Enter, and the box is not empty, add the book. Press **Escape** to clear the box.

## Passing Parameters to Event Handlers

Sometimes the handler needs extra information, such as which book was clicked. Wrap the call in an arrow function, and pass the value inside it.

```tsx
<button onClick={() => addToCart("Atomic Habits")}>Add</button>
```

The arrow function runs only on click. Then it calls `addToCart` with the book name.

      <button onClick={() => addToCart("The Alchemist")}>Add The Alchemist</button>{" "}
      <button onClick={() => addToCart("Atomic Habits")}>Add Atomic Habits</button>
      <p>Cart: {cart.length === 0 ? "empty" : cart.join(", ")}</p>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Both buttons use the same `addToCart` function. Each button sends its own book name.

You can also pass the event object along with your own value:

```tsx
<button onClick={(e) => addToCart("Atomic Habits", e)}>Add</button>
```

## Typing Event Handlers

In TypeScript, you can give the event object a proper type instead of `any`. Then TypeScript knows what is inside it, and your editor can suggest its parts.

React gives a type for each kind of event. You import them from `"react"`:

<div className="tscroll">

| Type | Use it for | Example element |
|---|---|---|
| `MouseEvent<HTMLButtonElement>` | Clicks and mouse events | `<button>` |
| `ChangeEvent<HTMLInputElement>` | Typing in a box | `<input>` |
| `KeyboardEvent<HTMLInputElement>` | Key presses | `<input>` |
| `FormEvent<HTMLFormElement>` | Submitting a form | `<form>` |

</div>

The part in `< >` is the kind of element. For example, `HTMLButtonElement` means a `<button>`.

      <input value={name} onChange={handleChange} onKeyDown={handleKeyDown} placeholder="Your name" />{" "}
      <button onClick={handleClick}>Clear</button>
      <p>Hello, {name || "guest"}!</p>
    </div>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Look at this line:

```tsx
function handleChange(e: ChangeEvent<HTMLInputElement>) {
```

It says: `e` is a change event that comes from an `<input>`. Because of this type, TypeScript knows that `e.target.value` is text.

If you write the handler inline, such as `onChange={(e) => setName(e.target.value)}`, you do not need a type. TypeScript works it out for you.

## Preventing Default Behavior

Some elements have a **default behavior**: something the browser does by itself.

- Submitting a `<form>` reloads the page.
- Clicking a link `<a>` opens another page.

To stop this, call **`e.preventDefault()`** in your handler. Then only your code runs.

      <input
        type="email"
        placeholder="Your email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />{" "}
      <button type="submit">Subscribe</button>
      <p>{message}</p>
    </form>
  );
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Type an email and press **Subscribe**. The page does not reload. Instead, the thank-you message appears.

Without `e.preventDefault()`, a normal web page would reload, and the message would be lost.

## Event Propagation

When you click an element, the click event does not stop there. It also goes up to the element's parent, then that parent's parent, and so on. This is called **event propagation**, or **bubbling**.

<figure className="react-fig ngc-fig">
<svg viewBox="0 0 520 250" role="img" aria-labelledby="fig-bubble-title" style={{ width: "100%", maxWidth: 600, height: "auto" }}>
  <title id="fig-bubble-title">Animation: a click on the button runs its handler, then bubbles up to the card and then to the page</title>
  <rect x="20" y="10" width="480" height="170" rx="10" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} /><text x="36" y="32" textAnchor="start" style={{ fill: "var(--ngc-purple)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Page (onClick)</text><rect x="50" y="46" width="420" height="120" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="66" y="68" textAnchor="start" style={{ fill: "var(--ngc-blue)", fontSize: 13, fontWeight: 700, fontFamily: "var(--sans)" }}>Book card (onClick)</text><rect x="190" y="108" width="140" height="40" rx="8" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }} /><text x="260" y="132" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>Button (onClick)</text>
  <circle r="8" style={{ fill: "var(--ngc-orange)" }}><animate attributeName="cy" values="108;108;108;46;46;10;10" keyTimes="0;0.1;0.3;0.42;0.55;0.67;1" dur="9s" repeatCount="indefinite" /><animate attributeName="cx" values="260;260;260;390;390;440;440" keyTimes="0;0.1;0.3;0.42;0.55;0.67;1" dur="9s" repeatCount="indefinite" /><animate attributeName="opacity" values="0;0;1;1;1;1;0" keyTimes="0;0.09;0.1;0.5;0.8;0.93;1" dur="9s" repeatCount="indefinite" /></circle>
  <rect x="20" y="190" width="480" height="54" rx="10" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} /><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.12;0.16;0.93;1" dur="9s" repeatCount="indefinite" /><text x="36" y="210" textAnchor="start" style={{ fill: "var(--ngc-green)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>1. Button clicked</text></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.42;0.46;0.93;1" dur="9s" repeatCount="indefinite" /><text x="200" y="210" textAnchor="start" style={{ fill: "var(--ngc-blue)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>2. Card clicked</text></g><g opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.67;0.71;0.93;1" dur="9s" repeatCount="indefinite" /><text x="350" y="210" textAnchor="start" style={{ fill: "var(--ngc-purple)", fontSize: 12, fontWeight: 700, fontFamily: "var(--sans)" }}>3. Page clicked</text></g><text x="260" y="234" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontSize: 11, fontWeight: 700, fontFamily: "var(--sans)" }}>e.stopPropagation() stops the event going up</text>
</svg>
<figcaption>The click starts at the button, then goes up to each parent with an onClick.</figcaption>
</figure>

So if the button **and** its parent card both have `onClick`, one click runs **both** handlers. Usually you do not want this.

To stop the event from going up, call **`e.stopPropagation()`** in the button's handler.

      <div className="card" onClick={() => add("Card clicked: open book page")}>
        <h3>Atomic Habits</h3>
        <button onClick={() => add("Button clicked: add to cart")}>
          Add (no stop)
        </button>{" "}
        <button
          onClick={(e) => {
            e.stopPropagation(); // the card will not hear this click
            add("Button clicked: add to cart");
          }}
        >
          Add (with stop)
        </button>
      </div>
      <button onClick={() => setLog([])}>Clear log</button>
      <ol>
        {log.map((line, i) => <li key={i}>{line}</li>)}
      </ol>
    </div>
  );
}
`,
    "App.css": `.card {
  border: 2px solid #0047ba;
  border-radius: 8px;
  padding: 4px 12px 12px;
  margin-bottom: 8px;
  cursor: pointer;
}
`,
    "main.tsx": `import { createRoot } from "react-dom/client";

createRoot(document.getElementById("root")!).render(<App />);
`,
  }}
/>

Press **Add (no stop)**. The log shows two lines: the button ran, and then the card ran too. Press **Add (with stop)**. The log shows only one line, because `e.stopPropagation()` stopped the click from reaching the card.

`e.preventDefault()` stops the browser's own action. `e.stopPropagation()` stops the event from going up to the parents. They do different jobs.

## Common mistakes

<div className="tscroll">

| Mistake | What you see | Fix |
|---|---|---|
| Calling the handler: `onClick={handleClick()}` | It runs when the page loads, not on click | Pass the function: `onClick={handleClick}` |
| Passing a value without an arrow: `onClick={add("Atomic Habits")}` | It runs at once, and may loop forever | Wrap it: `onClick={() => add("Atomic Habits")}` |
| Lower-case event name: `onclick` | Nothing happens on click | Use camelCase: `onClick` |
| Passing a string: `onClick="handleClick()"` | TypeScript error | Pass a function in braces |
| Forgetting `e.preventDefault()` in a form | The page reloads and your state is lost | Call `e.preventDefault()` first in `onSubmit` |
| A button inside a clickable card | One click runs both handlers | Call `e.stopPropagation()` in the button's handler |
| `onKeyDown` on a `<div>` that cannot get focus | Key presses do nothing | Put it on an `<input>`, or add `tabIndex={0}` |

</div>

## Check Yourself

## Exercises

## What to Remember

- An event handler is a function you pass to a prop like `onClick`. Pass the function, do not call it.
- Use an arrow function to pass a value: `onClick={() => add("Atomic Habits")}`.
- `e.preventDefault()` stops the browser's own action. `e.stopPropagation()` stops the event from going up.

Next: Conditional Rendering
