EduFlow
React Fundamentals
0%

Section 3 · State & Events

Handling Events

15 min3 min readCode playground

Lesson at a glance

Attach event handlers correctly, pass arguments to them, read the event object, control bubbling and default behaviour, and pass handlers down to child components as props.

  1. 1Pass a function, don’t call it
  2. 2Passing arguments
  3. 3Handlers as props
  4. 4The event object
  5. 5Bubbling and default behaviour
  6. 6Handlers are where side effects belong

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

State changes because something happens: a click, a key press, a form submission. React’s event handling is close to the DOM’s, with a few conventions that keep components readable and a couple of traps that catch everyone once.

Pass a function, don’t call it

You attach a handler by passing a function to a prop like onClick:

JSX
export default function ClearButton({ onClear }) {
  function handleClick() {
    onClear();
  }

  return <button onClick={handleClick}>Clear watched</button>;
}

The braces hold handleClick, the function itself. React keeps it and calls it when the click happens. Compare:

JSX
// Right: passes the function. React calls it on click.
<button onClick={handleClick}>Clear watched</button>

// Wrong: calls it immediately, during render.
<button onClick={handleClick()}>Clear watched</button>

The second version runs handleClick every time the component renders and passes its return value (undefined) as the handler. If the function sets state, you get an infinite loop and React stops it with “Too many re-renders”. When you see that error, look for parentheses inside an onSomething={…}.

Passing arguments

Most handlers in a list need to know which item was clicked. Wrap the call in an arrow function:

JSX
{movies.map((movie) => (
  <li key={movie.id}>
    {movie.title}
    <button onClick={() => onRemove(movie.id)}>Remove</button>
  </li>
))}

The arrow function is created during render but only runs on click, and when it does, it calls onRemove with the right id. Creating a small function per item is normal and cheap; don’t contort your code to avoid it.

Handlers as props

Components that own state pass handlers down. Children call them to report what happened. The conventions make this readable:

  • Inside a component, name handlers handleSomething: handleRemove, handleSubmit.
  • Props that receive handlers are named onSomething: onRemove, onToggle, matching built-in events like onClick.
src/App.jsx
export default function App() {
  const [movies, setMovies] = useState(INITIAL_MOVIES);

  function handleRemove(id) {
    setMovies(movies.filter((m) => m.id !== id));
  }

  return <MovieList movies={movies} onRemove={handleRemove} />;
}

MovieList has no idea how removal works. It only knows that when the user clicks Remove, it calls onRemove(id). The state, and the rules for changing it, stay in one place.

The event object

React calls your handler with an event object. It wraps the browser’s native event and has the same familiar API in every browser:

JSX
function handleKeyDown(e) {
  if (e.key === 'Escape') {
    setSelectedId(null);
  }
}

<main onKeyDown={handleKeyDown}>…</main>

The properties you’ll use most are e.target (the element the event started on), e.currentTarget (the element whose handler is running), e.key for keyboard events, and two methods: e.preventDefault() and e.stopPropagation().

Bubbling and default behaviour

Events bubble: a click on a button inside a card triggers the button’s handler, then the card’s, then any ancestor’s. That’s why the onKeyDown on <main> above catches Escape pressed while any button inside it has focus. Usually bubbling is what you want, but not always:

JSX
<article onClick={() => onOpen(movie.id)}>
  <h2>{movie.title}</h2>
  <button
    onClick={(e) => {
      e.stopPropagation();
      onRemove(movie.id);
    }}
  >
    Remove
  </button>
</article>

Without stopPropagation, clicking Remove would also open the movie’s details. (A whole-card click target is a mouse convenience; keep a real button or link for opening the details too, so keyboard users can reach it.) Keep this pattern for the cases that need it; reaching for it everywhere makes event flow hard to follow.

preventDefault is different: it stops the browser’s built-in behaviour, such as a link navigating or a form reloading the page. It doesn’t stop bubbling. You’ll use it on the Watchlist’s form in the next lesson.

Handlers are where side effects belong

Components must be pure while rendering, but event handlers don’t run during rendering. They run because the user did something, so they’re the right place for side effects: setting state, writing to localStorage, sending a request, logging analytics. When you’re unsure where some code should live, ask “what caused it?” If the answer is a specific user action, it goes in that action’s handler.

Next, you’ll handle the most important event in the Watchlist, the form submission that adds a movie, and learn how controlled inputs keep form fields and state in sync.

Key takeaways

  • Pass a function to onClick, as in onClick={handleClick}; writing handleClick() calls it during render.
  • To pass an argument, wrap the call in an arrow function, as in onClick={() => onRemove(movie.id)}.
  • Handlers receive an event object with target, key, preventDefault() and stopPropagation().
  • Name handlers handleX inside a component and callback props onX, so the data flow reads clearly.
  • Use real <button> elements for clickable things so keyboard and screen reader users can use them too.

Further reading

Check your understanding

4 questions

Question 1 of 4

<button onClick={setSelectedId(movie.id)}>Select</button> crashes the app with “Too many re-renders”. Why?

Question 2 of 4

MovieItem receives onRemove from its parent. The parent’s removeMovie(id) expects a movie id. Which is correct inside MovieItem?

Question 3 of 4

A movie card has onClick={openDetails}, and inside it a Remove button with onClick={() => onRemove(movie.id)}. Clicking Remove also opens the details. What’s the fix?

Question 4 of 4

Why should a clickable movie title be a <button> instead of a <div onClick={…}>?

Practice

Code playground

Select, remove and Escape

Clicking a movie’s title selects it. Two things are missing:

  1. Remove doesn’t remove anything. App’s handleRemove(id) expects a movie id; fix how MovieItem calls onRemove.
  2. Pressing Escape while focus is anywhere inside <main> should clear the selection, so #selected reads Selected: none.

When the selected movie is removed, the selection should also show none. (Look at how selected is calculated before adding any code for this.)

import { useState } from 'react';

const INITIAL = [
  { id: 'a1', title: 'Arrival' },
  { id: 'p2', title: 'Past Lives' },
  { id: 'd3', title: 'Dune: Part Two' },
];

function MovieItem({ movie, onSelect, onRemove }) {
  return (
    <li>
      <button className="title" onClick={() => onSelect(movie.id)}>
        {movie.title}
      </button>
      <button className="remove" onClick={onRemove}>
        Remove
      </button>
    </li>
  );
}

export default function App() {
  const [movies, setMovies] = useState(INITIAL);
  const [selectedId, setSelectedId] = useState(null);
  const selected = movies.find((m) => m.id === selectedId);

  function handleRemove(id) {
    setMovies(movies.filter((m) => m.id !== id));
  }

  // Clear the selection when Escape is pressed anywhere inside <main>

  return (
    <main>
      <p id="selected">Selected: {selected ? selected.title : 'none'}</p>
      <ul>
        {movies.map((movie) => (
          <MovieItem
            key={movie.id}
            movie={movie}
            onSelect={setSelectedId}
            onRemove={handleRemove}
          />
        ))}
      </ul>
    </main>
  );
}
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Preview

Console

(no output)

Checks

  • Clicking “Past Lives” selects it
  • Remove deletes Arrival and keeps the selection
  • Removing the selected movie resets the selection to none
  • Escape clears the selection