Handling Events
React's event system lets you respond to user interactions — clicks, key presses, form submissions, mouse movement — using familiar JavaScript patterns. The API is intentionally similar to plain HTML events, but with a few important differences that make it more consistent, more powerful, and better suited to component-based architecture.
The React Event System (Synthetic Events)
React doesn't attach event listeners directly to DOM elements. Instead it uses event delegation: a single listener is attached at the root of your React tree. When a user clicks a button, the native click event bubbles up to the root, where React intercepts it and dispatches a SyntheticEvent to the appropriate handler.
This has two practical benefits: it's more memory-efficient (one listener instead of thousands), and it normalizes browser inconsistencies behind a cross-browser API. You don't need to worry about event.which vs event.keyCode or IE quirks — React handles it.
camelCase Event Names
In HTML you write onclick, onchange, onsubmit. In React JSX, event attributes use camelCase:
HTML attribute | React prop | Fires when |
|---|---|---|
|
| Element is clicked |
|
| Input value changes |
|
| Form is submitted |
|
| Key is pressed down |
|
| Key is released |
|
| Mouse enters element |
|
| Mouse leaves element |
|
| Element receives focus |
|
| Element loses focus |
|
| Element is double-clicked |
Passing Handler Functions, Not Calling Them
You pass a reference to the handler function — you don't call it inline. React calls your function when the event fires.
// ✓ Correct — pass the function reference
<button onClick={handleClick}>Click me</button>
// ✗ Wrong — this calls handleClick() immediately during render
<button onClick={handleClick()}>Click me</button>The second form calls handleClick() while the JSX is evaluated — so it runs during render, not on click, which is almost never what you want.
A Basic Click Handler
import { useState } from 'react'
function LikeButton() {
const [likes, setLikes] = useState(0)
function handleClick() {
setLikes(prev => prev + 1)
}
return (
<button onClick={handleClick}>
♥ {likes} {likes === 1 ? 'like' : 'likes'}
</button>
)
}Form Submission and e.preventDefault()
HTML forms reload the page by default when submitted. In React apps you almost always want to prevent this and handle submission in JavaScript. Call e.preventDefault() at the start of your submit handler:
import { useState } from 'react'
function SearchForm() {
const [query, setQuery] = useState('')
const [result, setResult] = useState(null)
async function handleSubmit(e) {
e.preventDefault() // stop the browser from navigating
const data = await fetchSearch(query)
setResult(data)
}
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<button type="submit">Search</button>
</form>
)
}Keyboard Events
Use onKeyDown to respond to specific key presses. The event object provides e.key (the human-readable key name) and e.code (the physical key position). Prefer e.key for most use cases.
function ChatInput() {
const [message, setMessage] = useState('')
function handleKeyDown(e) {
// Submit on Enter, but allow Shift+Enter for newlines
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
sendMessage(message)
setMessage('')
}
}
return (
<textarea
value={message}
onChange={e => setMessage(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type a message... (Enter to send)"
/>
)
}Stopping Event Propagation
Events bubble up the DOM tree by default. A click on a child element also fires click handlers on every ancestor. Call e.stopPropagation() to stop the event from bubbling further up.
function Card({ onSelect, children }) {
return (
<div onClick={onSelect} className="card">
{children}
<button
onClick={e => {
e.stopPropagation() // prevent click from reaching the Card
handleDelete()
}}
>
Delete
</button>
</div>
)
}
// Without stopPropagation, clicking Delete would also trigger onSelect
// because the click event would bubble from <button> up to <div onClick={onSelect}>Inline Arrow Functions
When you need to pass arguments to a handler, an inline arrow function is the cleanest approach:
function TodoList({ todos, onDelete }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<button onClick={() => onDelete(todo.id)}>Remove</button>
</li>
))}
</ul>
)
}
// The arrow function () => onDelete(todo.id) closes over todo.id
// and passes it to onDelete when the button is clickedMouse Events
function Tooltip({ label, children }) {
const [visible, setVisible] = useState(false)
return (
<div
onMouseOver={() => setVisible(true)}
onMouseOut={() => setVisible(false)}
style={{ position: 'relative', display: 'inline-block' }}
>
{children}
{visible && (
<span className="tooltip">{label}</span>
)}
</div>
)
}Common Event Props Reference
onClick— button clicks, any clickable elementonChange— input, textarea, and select value changesonSubmit— form submission (attach to the<form>element)onKeyDown/onKeyUp/onKeyPress— keyboard interactionsonFocus/onBlur— focus management, showing/hiding validation errorsonMouseDown/onMouseUp/onMouseMove— drag interactionsonMouseOver/onMouseOut— hover effectsonMouseEnter/onMouseLeave— like hover but don't bubbleonScroll— scroll position trackingonInput— fires synchronously on every keystroke (less common thanonChange)onPaste/onCopy/onCut— clipboard operationsonDragStart/onDrop— drag-and-drop interfacesonTouchStart/onTouchEnd— mobile touch eventsonContextMenu— right-click context menuonAnimationEnd/onTransitionEnd— CSS animation lifecycle