Events
By now, the path from JSX to rendered output is visible:
- JSX creates React element descriptions
- those descriptions contain a
typeandprops - 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:
onPressis a prop for theButtoncomponentButtonchooses what that prop meansonClickis 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:
- JSX creates an element with an event prop such as
onClick. - React DOM turns that prop into a browser event listener on the real DOM node.
- The browser reports an interaction later.
- React calls the handler function with an event object.
- 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
- 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.