Skip to main content

Handling Events

Events are how users interact with your application. Every click, keystroke, form submission, and mouse movement fires an event that your components can respond to. React provides a consistent, cross-browser event system called Synthetic Events. Think of SyntheticEvents as a universal translator: no matter which browser your user is on (Chrome, Firefox, Safari), React normalizes the event into a single, consistent API so you never have to write browser-specific code.

React Events vs DOM Events

HTML:
React:

Basic Event Handling

Defining Event Handlers

Best Practice: Define handlers as separate functions for:
  • Better readability
  • Easier debugging
  • Potential performance benefits with useCallback

Don’t Call the Function!

This is one of the most common beginner mistakes in React. You need to pass a reference to a function, not the result of calling it.
Practical pitfall: If you see your handler firing on every render (or your app freezing in an infinite loop), check for accidental function invocation with (). This is the #1 debugging timesink for React beginners.

The Event Object

React wraps native browser events in a SyntheticEvent for cross-browser compatibility.

Common Event Properties


Passing Arguments to Event Handlers

Often you need to pass data (like an item ID) to the handler.

Accessing Event AND Custom Arguments


Mouse Events

Mouse Event Types


Keyboard Events

Key Event Properties

Building Keyboard Shortcuts


Form Events

Input Change Event

Focus Events


Event Propagation

Events bubble up through the DOM tree. You can stop this behavior.

Event Capturing

React also supports the capture phase (rarely needed):

Touch Events (Mobile)


Drag and Drop Events


Event Handler Pitfalls

Pitfall 1 — Inline arrow functions in lists cause unnecessary re-renders: When you write onClick={() => handleDelete(item.id)} inside a .map(), a new function is created for every item on every render. If the child component is wrapped in React.memo, this defeats memoization because the onClick prop is always a new reference. For large lists, extract the handler or use useCallback.Pitfall 2 — SyntheticEvent pooling (React 16 and earlier): In older React versions, event objects were pooled and reused. Accessing event.target inside a setTimeout or async function returned null because the event had been recycled. React 17+ eliminated pooling, but you may encounter this in legacy codebases. The fix was event.persist().Pitfall 3 — stopPropagation hiding bugs: Using event.stopPropagation() prevents an event from reaching parent handlers. This can silently break features like a “click outside to close” listener on the document, because the click event never reaches the document. Use it sparingly and document why.Pitfall 4 — Double-click interfering with single click: If you listen to both onClick and onDoubleClick on the same element, a double-click fires two onClick events before the onDoubleClick. To distinguish between them, use a timer: wait ~250ms after a click before acting, and cancel it if a double-click arrives.

🎯 Practice Exercises


Summary

Next Steps

In the next chapter, you’ll learn about Lists & Keys — efficiently rendering collections of data!

Interview Deep-Dive

Strong Answer: SyntheticEvents are React’s cross-browser wrapper around native DOM events. They exist because browser event implementations are inconsistent — different browsers name properties differently, bubble events differently, and handle default behaviors differently. React normalizes all of this into a single consistent API so you write event handling code once and it works everywhere.Under the hood, React does not attach event listeners to individual DOM nodes. Since React 17, it attaches a single event listener to the root DOM container (the element you pass to createRoot). When an event fires, it bubbles up to the root, React identifies which component the event target belongs to, creates a SyntheticEvent wrapper, and dispatches it through the React component tree. This is called event delegation, and it is more memory-efficient than attaching thousands of individual listeners.The key behavioral differences from native events: SyntheticEvents are pooled in React 16 and earlier (the event object is reused across handlers, so you cannot access it asynchronously without calling e.persist()). React 17+ removed pooling, so events are regular objects you can access anytime. Also, stopPropagation() on a SyntheticEvent stops propagation in React’s event system but does not stop the native event from propagating — this can cause subtle bugs if you mix React handlers with native addEventListener calls.Follow-up: You add a native event listener via addEventListener in a useEffect, and a React onClick handler on the same element. The stopPropagation in the React handler does not prevent the native listener from firing. Why?Because React 17+ attaches its delegated listener to the root container, not the individual element. When you click the element, the native event propagates through the real DOM first. Your addEventListener handler on the element fires during native propagation. Then the event reaches the root container, React intercepts it, creates a SyntheticEvent, and dispatches it through the React tree. e.stopPropagation() in the React handler stops propagation within React’s virtual event system, but the native propagation already happened.To prevent the native listener from firing, you need to call e.nativeEvent.stopImmediatePropagation() in the React handler, which stops the underlying native event. But this is fragile and usually signals an architectural problem — mixing React and native event handling on the same element is a code smell. Better to handle everything in React or everything natively, not both.
Strong Answer: Event delegation is the pattern of attaching a single event listener to a parent element and using event.target to determine which child triggered it. React uses this internally — instead of attaching an onClick to every button in your app, React attaches one click listener to a container and routes events to the correct component handlers.Before React 17, this single listener was attached to document. In React 17, it moved to the root DOM container (document.getElementById('root')). This change seems minor but solved real production problems.The main issue was apps that gradually migrated to React. If you had a jQuery section and a React section on the same page, React’s listener on document would intercept events from the jQuery section. stopPropagation() called in React would prevent non-React event listeners on document from seeing the event. Moving to the root container isolates React’s event system to its own DOM subtree.It also fixed micro-frontend architectures where multiple React roots coexist. With document-level delegation, two React apps would interfere with each other’s event handling. Container-level delegation means each React root manages its own events independently.In practice, the only code that broke during this migration was code that relied on React events propagating to document — for example, a document.addEventListener('click', closeDropdown) that expected to fire after React’s click handlers. After React 17, the native document listener fires before React’s container-level handler, changing the order.Follow-up: In a performance-critical scenario with 10,000 list items, each needing a click handler, what are the implications of React’s event delegation?Event delegation is actually the ideal solution for this case. React does not attach 10,000 click listeners — it attaches one to the root and routes clicks based on the target. Memory usage is constant regardless of list size, which is exactly what you want for large lists.The caveat is that the routing still involves React identifying which component the click target belongs to, which involves walking up the fiber tree. For 10,000 items this is still O(depth of tree), not O(n), so it is fast. Combined with virtualization (only rendering visible items), the event handling cost is negligible.Where you might see performance issues is if each list item’s click handler is a new arrow function created on every render (onClick={() => handleClick(item.id)}). This does not affect event delegation, but it defeats React.memo on list items because the function reference changes every render. The fix is useCallback or passing the ID to a stable handler that reads it from the event.
Strong Answer: event.target is the element that actually triggered the event — the deepest element in the DOM that was clicked, typed in, or interacted with. event.currentTarget is the element that the event handler is attached to. They are the same when you click directly on the element with the handler, but they differ when the event bubbles.Concrete bug scenario: you have a card component with an onClick handler on the outer div. Inside the card there is a title h3 and a description p. The handler reads event.target.dataset.cardId to determine which card was clicked. If the user clicks on the h3, event.target is the h3, which does not have data-card-id — so dataset.cardId is undefined. The handler fails silently or navigates to the wrong route. The fix is to use event.currentTarget.dataset.cardId, which always refers to the div that has the handler (and the data attribute).This comes up constantly in list interfaces. You have a row with onClick on the <tr>, and inside there are <td> elements. event.target could be any <td> or even a <span> inside a <td>. If your handler assumes event.target is the <tr>, you will read wrong data.Another scenario: styling. You have event.target.classList.add('active') in an onClick handler on a container. If the user clicks a child element, you add the class to the child instead of the container. Use event.currentTarget for the element with the handler.Follow-up: How do capture and bubble phases work in React, and when would you use onClickCapture?In native DOM events, there are three phases: capture (event travels from document down to the target), target (event reaches the element), and bubble (event travels back up). React supports both capture and bubble phases through event props: onClick fires during the bubble phase, onClickCapture fires during the capture phase.The capture phase fires before the bubble phase, so onClickCapture on a parent fires before onClick on the child. This is useful when you need a parent to intercept an event before any child sees it.Practical use case: implementing a focus trap in a modal. You add onFocusCapture on the modal container to intercept any focus event before it reaches the target. If the focus target is outside the modal, you redirect focus back inside. Another use case: analytics logging at the app root that captures all clicks before any component’s handler might call stopPropagation. By using the capture phase, your logger always fires regardless of what child handlers do.