How to Implement a React onClick Event Handler on Custom Components for Child Element Clicks
React's synthetic event system automatically routes clicks from any nested child element to a parent component's onClick handler through the React Fiber tree, allowing custom components to capture events without manual DOM listener management.
React's event handling architecture makes it straightforward to implement an onclick event handler on custom components that responds to interactions originating from any descendant element. According to the facebook/react source code, the framework uses a delegation model where a single native listener registered at the document level captures all clicks and dispatches synthetic events that bubble through the component hierarchy.
How React's Synthetic Event System Routes Child Clicks to Parent Handlers
React does not attach individual click listeners to every DOM element. Instead, the framework implements event delegation through its Synthetic Event system. In packages/react-dom-bindings/src/events/ReactDOMEventListener.js, React registers top-level native listeners at the document root that capture all interactions within the application.
When a user clicks any element inside your custom component, the native event triggers the document-level listener. React then creates a SyntheticMouseEvent (implemented in packages/react-dom-bindings/src/events/SyntheticEvent.js and packages/react-dom-bindings/src/events/plugins/SimpleEventPlugin.js) and dispatches it through the React Fiber tree. This synthetic event bubbles upward from the event target through every component in the hierarchy, invoking any onClick handlers defined on the path.
Key architectural requirements for this behavior:
- Host node requirement: The custom component must render a real DOM element (like
<div>or<button>) that can serve as an event target. Fragments andnullreturns cannot receive delegated events. - Automatic propagation: The synthetic event traverses the React component tree regardless of how deeply nested the actual click originated.
- Event identity: The
event.targetproperty references the actual DOM node that received the native click, whileevent.currentTargetreferences the node where you attached the handler.
Implementing onClick on Custom Components with Child Elements
Basic Functional Component Pattern
To make a custom component respond to clicks from any child, pass the onClick prop to the root host element:
function Card({ onClick, children }) {
// The root <div> receives the delegated click from any descendant
return (
<div className="card" onClick={onClick}>
{children}
</div>
);
}
// Usage: Clicking the button triggers Card's onClick
<Card onClick={() => console.log('Card clicked')}>
<button>Click me</button>
<span>Or click here</span>
</Card>
The onClick prop is attached to the <div> in packages/react-dom-bindings/src/events/EventRegistry.js during the render phase. When any child element fires a native click, the synthetic event bubbles up to this host node and triggers your handler.
Identifying Which Child Element Was Clicked
To execute logic based on the specific child that was clicked, inspect the event.target property within your handler:
class ListItem extends React.Component {
handleClick = (event) => {
// event.target is the actual DOM node that received the click
if (event.target.matches('button.delete')) {
this.props.onDelete(this.props.id);
} else if (event.target.closest('.selectable')) {
this.props.onSelect(this.props.id);
}
};
render() {
return (
<li onClick={this.handleClick}>
<span className="selectable">{this.props.title}</span>
<button className="delete">Delete</button>
</li>
);
}
}
This pattern leverages the fact that SyntheticMouseEvent (defined in packages/react-dom-bindings/src/events/SyntheticEvent.js) preserves the standard DOM target reference, allowing you to differentiate between clicks on the title span and the delete button even though both trigger the same onClick handler on the parent <li>.
Preserving Event Handlers with Forwarded Refs
When wrapping native elements, use React.forwardRef to ensure the onClick prop reaches the underlying host node:
const FancyButton = React.forwardRef(({ onClick, label, children }, ref) => (
<button ref={ref} className="fancy" onClick={onClick}>
<span className="icon">{children}</span>
{label}
</button>
));
// Usage: Clicks on the inner <span> bubble to the button's onClick
<FancyButton onClick={handleClick} label="Submit">
<svg>...</svg>
</FancyButton>
Even though the component renders an internal <span> for the icon, the onClick attached to the <button> in packages/react-dom-bindings/src/events/DOMEventProperties.js receives clicks from any descendant, including the SVG icon.
Stopping Event Propagation to Parent Components
To prevent a child click from reaching parent component handlers, call stopPropagation() on the synthetic event:
function StopPropagationButton({ onClick }) {
const handleClick = (event) => {
event.stopPropagation(); // Prevent bubbling to parent onClick
onClick?.();
};
return <button onClick={handleClick}>Don't bubble up</button>;
}
// Usage
<Card onClick={() => console.log('This will not fire when button is clicked')}>
<StopPropagationButton onClick={() => console.log('Button clicked')} />
</Card>
The stopPropagation() method is defined on the base SyntheticEvent class in packages/react-dom-bindings/src/events/SyntheticEvent.js and prevents the event from traversing further up the React Fiber tree.
Key Implementation Files in the React Codebase
-
packages/react-dom-bindings/src/events/ReactDOMEventListener.js: Registers the single document-level native listener that captures all click events and initiates the synthetic event creation process. -
packages/react-dom-bindings/src/events/plugins/SimpleEventPlugin.js: Contains the logic for creatingSyntheticMouseEventinstances from native click, auxclick, and dblclick events. -
packages/react-dom-bindings/src/events/SyntheticEvent.js: Defines the base synthetic event class includingpreventDefault(),stopPropagation(), and thetarget/currentTargetproperties. -
packages/react-dom-bindings/src/events/EventRegistry.js: Maps React event names likeonClickto their registration names used during the event delegation process. -
packages/react-dom-bindings/src/events/DOMEventProperties.js: Lists all supported native events and configures them for synthetic handling in the React DOM.
Summary
- React implements event delegation through a single document-level listener in
ReactDOMEventListener.js, not individual element listeners. - SyntheticMouseEvent objects bubble through the React Fiber tree, allowing parent custom components to capture clicks from any nested child.
- Custom components must render a host DOM node (not a Fragment) to receive delegated
onClickevents. - Use
event.targetto identify the specific child element that triggered the click. - Call
event.stopPropagation()in child handlers to prevent the event from reaching parent componentonClickprops.
Frequently Asked Questions
Does React attach onClick listeners directly to the DOM element I specify?
No. According to the implementation in ReactDOMEventListener.js, React registers one native click listener at the document root. When you specify onClick on a component, React stores this in the component's fiber node and invokes it during the synthetic event's bubbling phase through the React tree, not through direct DOM attachment.
How can I tell which specific child element triggered the onClick in my parent component?
Inspect the event.target property inside your handler. This references the actual DOM node that received the native click, even if your handler is defined on a parent component. You can use standard DOM methods like matches() or closest() to determine which child component was clicked.
Why doesn't my custom component's onClick fire when I return a Fragment or null?
React's synthetic event system only bubbles across host nodes (real DOM elements like div or button). If your component returns a Fragment (<>...</>), an array, or null, there is no host node to attach the event listener to. You must wrap your content in a DOM element to receive delegated click events from children.
What's the difference between event.target and event.currentTarget in React synthetic events?
event.target refers to the DOM node that actually received the native event (the deepest child clicked). event.currentTarget refers to the DOM node where you attached the onClick handler (the component's root element). Both properties are set correctly by the Synthetic Event system in SyntheticEvent.js during the event dispatch phase.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →