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.
- 1Pass a function, don’t call it
- 2Passing arguments
- 3Handlers as props
- 4The event object
- 5Bubbling and default behaviour
- 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:
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:
// 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:
{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 likeonClick.
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:
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:
<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 inonClick={handleClick}; writinghandleClick()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()andstopPropagation(). - Name handlers
handleXinside a component and callback propsonX, so the data flow reads clearly. - Use real
<button>elements for clickable things so keyboard and screen reader users can use them too.