Events

Publish at:

By now, the path from JSX to rendered output is visible:

  • JSX creates React element descriptions
  • those descriptions contain a type and props
  • the renderer turns host elements into DOM nodes
  • components receive props as their input

But all of that still sounds static. A component returns a description, React renders it, and then the user is sitting in front of the page.

That leaves the next hole:

if UI descriptions are static values, how does user interaction enter the system?

The browser's shape #

The browser already has an event system. Without React, we can create a button, attach a listener, and let the browser call that listener later[3].

button.addEventListener('click', () => {
  console.log('clicked');
});

That tells us the basic shape we need:

  • there is a DOM node
  • something happens to that node
  • a function runs later

React has to express that same idea through the element descriptions we already know.

So the next question is:

where does the event handler live in a React element?

First wrong shape #

Since the browser event is named click, a natural first guess is to write a click attribute.

function App() {
  return (
    <button click={() => console.log('clicked')}>
      Try click
    </button>
  );
}
root.render(<App />);
setTimeout(() => {
  mountNode.querySelector('button').click();
}, 20);

Waiting to run

Not run yet.

That does not give React a usable click handler.

This is the first useful constraint. In JSX, event handlers are not written as lowercase DOM attribute names. React expects a specific prop name for the event.

For a click, that prop is onClick[1].

Handler props #

The smallest corrected version is:

function App() {
  return (
    <button onClick={() => console.log('clicked')}>
      Try click
    </button>
  );
}
root.render(<App />);
setTimeout(() => {
  mountNode.querySelector('button').click();
}, 20);

Waiting to run

Not run yet.

Now the function runs.

This fits the props model. onClick is a JSX attribute, so it becomes a field on the element's props object.

We can see that directly:

const element = (
  <button onClick={() => console.log('clicked')}>
    Inspect me
  </button>
);
console.log(element.props);
root.render(element);

Waiting to run

Not run yet.

The handler is just a function stored in props.

So the first definition is:

a React event handler is a function passed through a specially named prop such as onClick

The renderer decides what to do with that prop when it creates the real DOM node.

Passing a function, not calling one #

There is a small trap here. We want React to call the function later, when the event happens. That means we must pass a function value. If we call the function while rendering, the timing is wrong:

function announce() {
  console.log('called during render');
}
function App() {
  return (
    <button onClick={announce()}>
      Try click
    </button>
  );
}
root.render(<App />);
setTimeout(() => {
  mountNode.querySelector('button').click();
}, 20);

Waiting to run

Not run yet.

The log happened while React was rendering. The click did not cause it.

The expression announce() calls the function immediately and uses its return value as the onClick prop. Since announce returns nothing, the button ends up with no useful handler.

The fixed version passes the function itself:

<button onClick={announce}>Try click</button>

Or, when we need to supply arguments, it passes a new function that calls the real function later:

<button onClick={() => announce('Ada')}>Try click</button>

So the rule is:

event props receive functions to call later, not the result of calling those functions now

The event object #

When React calls an event handler, it gives the handler an event object[2]. That object tells us what happened.

function App() {
  function handleClick(event) {
    console.log('type:', event.type);
    console.log('current target:', event.currentTarget.tagName);
    console.log('button text:', event.currentTarget.textContent);
  }
  return (
    <button onClick={handleClick}>
      Inspect event
    </button>
  );
}
root.render(<App />);
setTimeout(() => {
  mountNode.querySelector('button').click();
}, 20);

Waiting to run

Not run yet.

The important part is the direction:

  • React renders a description
  • the browser later reports an event
  • React calls the handler function
  • the handler receives information about the event

That is how something outside the render description enters the program.

Custom components #

There is one more bridge to make precise. For a host element such as <button>, React DOM knows that onClick should become a browser click listener. But for a custom component, props are just props. React does not automatically know which DOM node inside that component should receive the handler.

This first attempt loses the click:

function Button(props) {
  return <button>{props.children}</button>;
}
root.render(
  <Button onPress={() => console.log('pressed')}>
    Press
  </Button>
);
setTimeout(() => {
  mountNode.querySelector('button').click();
}, 20);

Waiting to run

Not run yet.

onPress reached the Button component as a prop, but the component did not use it. The component has to pass that function to the host element:

function Button(props) {
  return (
    <button onClick={props.onPress}>
      {props.children}
    </button>
  );
}
root.render(
  <Button onPress={() => console.log('pressed')}>
    Press
  </Button>
);
setTimeout(() => {
  mountNode.querySelector('button').click();
}, 20);

Waiting to run

Not run yet.

This is the same props rule again:

  • onPress is a prop for the Button component
  • Button chooses what that prop means
  • onClick is the prop React DOM understands on the real <button>

For custom components, event-like prop names are conventions until the component gives them meaning.

Propagation #

Browser events might belong to the entire hierarchy of elements. A click can also travel through ancestor elements in the DOM tree[4]. React follows that model for common events.

function App() {
  return (
    <section onClick={() => console.log('section handler')}>
      <button onClick={() => console.log('button handler')}>
        Click inside
      </button>
    </section>
  );
}
root.render(<App />);
setTimeout(() => {
  mountNode.querySelector('button').click();
}, 20);

Waiting to run

Not run yet.

Both handlers run because the click starts at the button and then propagates upward.

If a child handler needs to stop that upward travel, it can call event.stopPropagation():

function App() {
  return (
    <section onClick={() => console.log('section handler')}>
      <button
        onClick={(event) => {
          event.stopPropagation();
          console.log('button handler');
        }}
      >
        Click inside
      </button>
    </section>
  );
}
root.render(<App />);
setTimeout(() => {
  mountNode.querySelector('button').click();
}, 20);

Waiting to run

Not run yet.

That gives us a sharper model:

React event handlers are attached through props, but the event still comes from a real browser interaction with the rendered DOM

Interaction is not state #

Now we can run code when a user interacts with the page.

It is tempting to think this is already enough to make the UI change. But an event handler only runs code. It does not automatically replace the rendered description.

Here is a deliberately incomplete counter:

let count = 0;
function App() {
  function handleClick() {
    count += 1;
    console.log('count is now', count);
  }
  return (
    <button onClick={handleClick}>
      Count: {count}
    </button>
  );
}
root.render(<App />);
setTimeout(() => {
  const button = mountNode.querySelector('button');
  button.click();
  button.click();
  console.log('button text:', button.textContent);
}, 20);

Waiting to run

Not run yet.

The handler ran. The variable changed. But the rendered button text still came from the element description React already produced.

This exposes the next hole:

where does changing local data live so React knows to render again?

That is the job of state, not events.

Filling the hole #

Events give React programs a way to respond after rendering.

The path looks like this:

  1. JSX creates an element with an event prop such as onClick.
  2. React DOM turns that prop into a browser event listener on the real DOM node.
  3. The browser reports an interaction later.
  4. React calls the handler function with an event object.
  5. The handler can run code, but it does not by itself define the next UI.

That means events sit exactly at the boundary we needed:

rendering describes what should exist; events report what happened after it exists

Final definition #

An event handler is a function passed through a React event prop such as onClick. React DOM attaches that handler to the rendered DOM node and calls it later when the matching browser event happens.

For custom components, event-like names are ordinary props until the component passes them to a host element or calls them itself.

Summary #

We started with the question of how interaction enters a UI made from descriptions. The answer is that event handlers are functions carried by props.

That process gives us the model:

  • React event names use camelCase props such as onClick
  • the value must be a function to call later
  • React passes an event object to the handler
  • custom components must decide how to use event-like props
  • events can propagate through the rendered DOM tree
  • an event handler can change ordinary variables, but that alone does not make React render a new UI

So events answer "what happened?" The next article needs to answer:

where does changing local data live?

Notes

  1. The live demos in this article use the React 19.2.5 and react-dom 19.2.5 development bundles. Some snippets trigger clicks programmatically so their event output can be captured in the article.

References

  1. React: Responding to Events (opens in a new tab) · Back
  2. React DOM common components: React event object (opens in a new tab) · Back
  3. MDN: `EventTarget.addEventListener()` (opens in a new tab) · Back
  4. MDN: Event bubbling (opens in a new tab) · Back