Event Handling in React
How React Events Differ from Vanilla JavaScript
If you’re coming from vanilla JavaScript, React’s event system feels familiar but has some key differences. React doesn’t attach event listeners directly to the elements you create — instead, it uses a single root-level listener and delegates all events. This is called the Synthetic Event System.
Here’s the most immediate difference in naming:
// Vanilla JS — lowercase, string handler
<button onclick="handleClick()">Click</button>
// React — camelCase, function reference
<button onClick={handleClick}>Click</button>
In vanilla JS, onclick accepts a string that gets evaluated, or a function reference via addEventListener. In React, you pass the function object directly as a JSX prop. No quotes, no addEventListener call needed.
Another critical difference is onChange. In the browser, a traditional onchange event only fires when an input loses focus after its value changed. In React, onChange fires on every keystroke — the same behavior as the browser’s oninput event. If you expect vanilla JS behavior from React’s onChange, you’ll be surprised.
React also normalizes events across browsers. Chrome, Firefox, Safari, and Edge all have subtle inconsistencies in how they expose events. React’s synthetic events give you a consistent API regardless of which browser your app runs in.
Handling Button Clicks with onClick
The onClick handler is the most common event you’ll use. You pass it a function that React calls when the element is clicked.
function ButtonClick() {
function handleClick() {
console.log('Button clicked at', new Date().toISOString());
// output: Button clicked at 2026-03-27T10:30:00.000Z
}
return <button onClick={handleClick}>Click me</button>;
}
You can also define the handler inline if it’s short:
function InlineButton() {
return (
<button onClick={() => console.log('Clicked!')}>
Click
</button>
);
}
Be careful not to call the function by mistake with parentheses:
// Wrong — calls handleClick immediately on every render
<button onClick={handleClick()}>Wrong</button>
// Correct — passes the function reference
<button onClick={handleClick}>Right</button>
Common Event Attributes
Beyond onClick, React supports a wide range of event attributes matching the browser’s event model, all in camelCase.
onChange — Input Changes
function SearchInput() {
const [query, setQuery] = React.useState('');
function handleChange(e) {
setQuery(e.target.value);
}
return (
<div>
<input
value={query}
onChange={handleChange}
placeholder="Search..."
/>
<p>You typed: {query}</p>
</div>
);
}
onSubmit — Form Submission
function SimpleForm() {
function handleSubmit(e) {
e.preventDefault();
console.log('Form submitted!');
// output: Form submitted!
}
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Your name" />
<button type="submit">Send</button>
</form>
);
}
onFocus and onBlur — Focus Management
function FocusDemo() {
const [focused, setFocused] = React.useState(false);
return (
<input
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder={focused ? 'Focused!' : 'Click here'}
/>
);
}
onKeyDown — Keyboard Events
function KeyLogger() {
const [lastKey, setLastKey] = React.useState('');
function handleKeyDown(e) {
setLastKey(e.key);
console.log('Key:', e.key, 'Code:', e.code);
}
return (
<input
onKeyDown={handleKeyDown}
placeholder="Press any key"
/>
);
}
Passing Arguments to Event Handlers
Sometimes you need to pass extra data to your handler. There are two common ways.
Arrow Functions
Wrap the handler in an arrow function:
function ItemList() {
function handleClick(id) {
console.log('Clicked item:', id);
// output: Clicked item: 42
}
const items = [
{ id: 1, name: 'Apple' },
{ id: 42, name: 'Banana' },
];
return (
<ul>
{items.map(item => (
<li key={item.id}>
<button onClick={() => handleClick(item.id)}>
{item.name}
</button>
</li>
))}
</ul>
);
}
The bind Method
bind() pre-fills arguments. The event object always passes as the last argument automatically:
function HandleClick(id) {
function handleClick(id, event) {
console.log('ID:', id, 'Target:', event.target.tagName);
// output: ID: 7 Target: BUTTON
}
return <button onClick={handleClick.bind(null, 7)}>Click 7</button>;
}
Passing Both the Event and Extra Data
function handleClick(event, extraData) {
console.log('Event type:', event.type); // 'click'
console.log('Extra:', extraData); // { foo: 'bar' }
}
<button onClick={(e) => handleClick(e, { foo: 'bar' })}>
Click
</button>
The Event Object
React passes a synthetic event object as the first argument to every handler. It has all the properties you’d expect from a native event, normalized for cross-browser consistency.
function EventDemo() {
function handleClick(e) {
console.log('target:', e.target.tagName); // BUTTON
console.log('currentTarget:', e.currentTarget.tagName); // BUTTON
console.log('type:', e.type); // click
console.log('defaultPrevented:', e.defaultPrevented); // false
}
return <button onClick={handleClick}>Click me</button>;
}
preventDefault
Stop the browser’s default behavior:
function StopDefault() {
function handleClick(e) {
e.preventDefault();
console.log('Default prevented, no navigation');
}
return (
<a href="https://example.com" onClick={handleClick}>
Go to Example
</a>
);
}
stopPropagation
Stop the event from bubbling up to parent elements:
function StopProp() {
function handleDivClick() {
console.log('Div clicked');
}
function handleButtonClick(e) {
e.stopPropagation();
console.log('Button clicked only');
}
return (
<div onClick={handleDivClick}>
<button onClick={handleButtonClick}>Click me</button>
</div>
);
}
Without stopPropagation, clicking the button would log both “Button clicked only” and “Div clicked”. With it, only the button’s handler fires.
Working with Forms: Controlled Components
In React, a controlled component is an input whose value lives entirely in state. Every change updates state, and state updates re-render the input with the new value.
Text Input
function TextForm() {
const [value, setValue] = React.useState('');
function handleChange(e) {
setValue(e.target.value);
}
function handleSubmit(e) {
e.preventDefault();
if (value.trim() === '') {
console.log('Please enter a value');
return;
}
console.log('Submitted:', value);
setValue('');
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={value}
onChange={handleChange}
placeholder="Your name"
/>
<button type="submit">Submit</button>
</form>
);
}
Select Dropdown
function FruitSelect() {
const [fruit, setFruit] = React.useState('apple');
return (
<select value={fruit} onChange={(e) => setFruit(e.target.value)}>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="cherry">Cherry</option>
</select>
);
}
Checkbox
function ToggleCheckbox() {
const [checked, setChecked] = React.useState(false);
return (
<label>
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
{checked ? 'Enabled' : 'Disabled'}
</label>
);
}
stopPropagation with Nested Elements
Event bubbling is one of the most common sources of confusion in React. When an event fires on a child, it bubbles up to all parent elements that have listeners for that event type.
function NestedDemo() {
function handleCardClick() {
console.log('Card clicked');
}
function handleButtonClick(e) {
e.stopPropagation();
console.log('Button clicked — card handler blocked');
}
return (
<div onClick={handleCardClick} style={{ padding: 20, background: '#eee' }}>
<p>Click the button, not the card</p>
<button onClick={handleButtonClick}>Do something</button>
</div>
);
}
If you forget stopPropagation, clicking the button logs both messages. This matters a lot when you’re building things like modal overlays where clicking the backdrop should close the modal, but clicking inside the modal content should not.
Custom Events in React
Native browser custom events don’t map to JSX props. You can’t write <div onMyEvent={handler}>. Instead, you attach and dispatch them directly on DOM elements using a ref.
function CustomEventDemo() {
const ref = React.useRef(null);
const [log, setLog] = React.useState([]);
React.useEffect(() => {
function handleGreet(e) {
setLog(prev => [...prev, e.detail.message]);
}
const el = ref.current;
el.addEventListener('greet', handleGreet);
return () => el.removeEventListener('greet', handleGreet);
}, []);
function triggerGreet() {
const event = new CustomEvent('greet', {
detail: { message: 'Hello from custom event!' },
bubbles: true,
});
ref.current.dispatchEvent(event);
}
return (
<div ref={ref}>
<button onClick={triggerGreet}>Say Hello</button>
{log.map((msg, i) => <p key={i}>{msg}</p>)}
</div>
);
}
Custom events are useful for communication between unrelated parts of your component tree, or for integrating third-party libraries that emit events.
Summary
React’s event handling system wraps browser events in a Synthetic Event layer that normalizes behavior across browsers and keeps your JSX clean.
Key things to remember:
- Naming is camelCase —
onClick,onChange,onSubmit, not lowercase - Pass function references —
onClick={handleClick}, notonClick={handleClick()} - Event object is the first argument — access it as
ein your handler - Call
e.preventDefault()to stop browser defaults like form submission or link navigation - Call
e.stopPropagation()to prevent events from bubbling to parent elements - Controlled components drive form inputs entirely through state with
onChangeupdating state and thevalueprop setting the display - Custom events require a DOM ref and
addEventListener— they don’t work as JSX props
See Also
- React: Intro and Setup — Set up your first React project from scratch
- React: Components and JSX — Build reusable UI pieces with JSX syntax
- React: Props and State — Learn how data flows through a React application