AniUI Academy

Handling Events

Attaching event handlers in JSX, the SyntheticEvent wrapper, passing arguments to a handler, and why event handling is a normal JavaScript function underneath.

8 min read

Interactivity starts with responding to what the user does — clicks, typing, submitting a form. React's event handling looks close to plain DOM events, with a few deliberate differences worth understanding.

Attaching a handler

Event props follow a onEventName naming convention, camelCased, and take a function:

function LikeButton() {
  function handleClick() {
    console.log("Liked!");
  }
 
  return <button onClick={handleClick}>Like</button>;
}

The critical detail: onClick={handleClick} passes the function itself — React calls it later, when the click actually happens. Writing onClick={handleClick()} instead calls the function immediately, during render, and passes whatever it returns (often undefined) as the handler. This is one of the most common early mistakes, and it fails silently — no error, just a button that does nothing on click and already ran its "handler" once on every render.

Passing arguments

A named handler function takes no arguments by default beyond the event object React passes automatically. To pass your own data — which item was clicked, for instance — wrap the call in an inline arrow function:

function TrackList({ tracks, onDelete }) {
  return (
    <ul>
      {tracks.map((track) => (
        <li key={track.id}>
          {track.title}
          <button onClick={() => onDelete(track.id)}>Remove</button>
        </li>
      ))}
    </ul>
  );
}

The arrow function () => onDelete(track.id) is itself the thing React calls on click; it just happens to call onDelete with an argument from the surrounding closure when it runs.

The event object

Handlers receive an event object as their argument, same as vanilla DOM code:

function SearchForm({ onSearch }) {
  function handleSubmit(event) {
    event.preventDefault(); // stop the page from reloading
    const query = new FormData(event.target).get("query");
    onSearch(query);
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <input name="query" />
      <button type="submit">Search</button>
    </form>
  );
}

React calls this a SyntheticEvent — a cross-browser wrapper around the underlying native event, exposing a consistent API (preventDefault, stopPropagation, target, and so on) regardless of which browser is running the code. Historically (before React 17) these objects were pooled and reused for performance, which meant accessing event properties asynchronously (after an await, for example) could read stale, nulled-out data unless you called event.persist() first. React 17 removed event pooling entirely, so in current React the event behaves like a normal object you can reference asynchronously without special handling — a real difference to know if you ever read older React code or tutorials that mention persist().

Where the listener actually lives

Another React 17 change worth knowing, purely for reading older material: React attaches one delegated event listener per event type to the root container your app renders into, rather than to individual DOM nodes. Before React 17, that delegation point was document itself; React 17 moved it to the root container. Practically, this rarely changes how you write handlers — it mostly matters if an app mixes multiple independent React roots (or React versions) on the same page, since events no longer bubble past a root's own container into a document-level listener belonging to a different root.

Try it yourself

A small form using both a click handler with a passed argument and a submit handler that reads the event:

Try it yourself
Loading playground...

What to remember

  • Pass the function itself to an event prop (onClick={handleClick}), never call it (onClick={handleClick()}).
  • Wrap a handler in an inline arrow function to pass your own arguments alongside the event.
  • The event object is a SyntheticEvent — a cross-browser wrapper with a preventDefault/stopPropagation API; since React 17 it is no longer pooled, so it's safe to reference asynchronously.
  • Modern React delegates listeners to the root container it renders into, not to document or to each element individually.

Check yourself

4 questions · pass 3/4 to unlock The Virtual DOM and Reconciliation

up to 50
  1. 1.What's wrong with <button onClick={handleClick()}>Save</button>?

  2. 2.How do you pass an argument to an event handler, like which item was clicked?

  3. 3.What is a React SyntheticEvent?

  4. 4.Where does React actually attach its event listeners in modern versions (React 17+)?

4 left to answer