The Synthetic Event Object
Every React event handler receives an event object as its first argument. React wraps the native browser event in a SyntheticEvent — a cross-browser wrapper that gives you a consistent API regardless of which browser your user is running.
What Is a SyntheticEvent?
SyntheticEvent is a React class that wraps the native Event object. It implements the W3C spec, so it behaves identically to native events but with one guarantee: the same properties and methods work in every browser. React handles all the normalisation behind the scenes.
function handleClick(e) {
// e is a SyntheticEvent, not the raw MouseEvent
console.log(e.type) // "click"
console.log(e.target) // the DOM element that was clicked
console.log(e.currentTarget) // the element the handler is attached to
console.log(e.nativeEvent) // the underlying native browser Event
}e.target vs e.currentTarget
These two properties are easily confused:
e.target— the element that originated the event (where the user clicked/typed)e.currentTarget— the element the handler is attached to (the element with theonClickprop)
function Container() {
function handleClick(e) {
console.log('target:', e.target.tagName) // could be SPAN, IMG, etc.
console.log('currentTarget:', e.currentTarget.tagName) // always DIV
}
return (
<div onClick={handleClick}>
<span>Click me</span>
<img src="logo.png" alt="" />
</div>
)
}
// Clicking the span:
// target: SPAN
// currentTarget: DIV
// Clicking the image:
// target: IMG
// currentTarget: DIVReading Input Values with e.target.value
The most common use of e.target is reading the current value of an input field. e.target.value is a string containing whatever the user has typed.
import { useState } from 'react'
function NameInput() {
const [name, setName] = useState('')
function handleChange(e) {
setName(e.target.value) // e.target is the <input>, .value is its text
}
return (
<div>
<input
type="text"
value={name}
onChange={handleChange}
placeholder="Enter your name"
/>
<p>Hello, {name || 'stranger'}!</p>
</div>
)
}Keyboard Event Properties
When handling keyboard events (onKeyDown, onKeyUp, onKeyPress), the event object provides several properties for identifying which key was pressed:
Property | Type | Example | Notes |
|---|---|---|---|
| string |
| Human-readable key name — prefer this |
| string |
| Physical key position, layout-independent |
| number |
| Deprecated — use |
| number |
| Deprecated — use |
| boolean |
| Was Shift held? |
| boolean |
| Was Ctrl held? |
| boolean |
| Was Alt/Option held? |
| boolean |
| Was Cmd/Win held? |
| boolean |
| Is the key held down (repeating)? |
function KeyboardDemo() {
function handleKeyDown(e) {
// Cmd+S or Ctrl+S — save shortcut
if (e.key === 's' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
saveDraft()
}
// Escape — close modal
if (e.key === 'Escape') {
closeModal()
}
// Arrow keys — navigate list
if (e.key === 'ArrowDown') {
selectNext()
}
if (e.key === 'ArrowUp') {
selectPrev()
}
}
return <div onKeyDown={handleKeyDown} tabIndex={0} />
}Mouse Event Properties
function ClickCoordinates() {
const [pos, setPos] = useState({ x: 0, y: 0 })
function handleClick(e) {
setPos({
x: e.clientX, // X position relative to the viewport
y: e.clientY, // Y position relative to the viewport
})
console.log('pageX:', e.pageX) // X relative to the full page
console.log('screenX:', e.screenX) // X relative to the screen
console.log('button:', e.button) // 0=left, 1=middle, 2=right
console.log('buttons:', e.buttons) // bitmask of currently pressed buttons
}
return (
<div onClick={handleClick} style={{ height: 200, background: '#f0f0f0' }}>
<p>Click anywhere</p>
<p>Last click: ({pos.x}, {pos.y})</p>
</div>
)
}e.preventDefault() and e.stopPropagation()
These two methods are available on every SyntheticEvent:
// e.preventDefault() — stops the browser's default action
// e.g., prevent form reload, prevent link navigation, prevent right-click menu
function LinkWithConfirm({ href, children }) {
function handleClick(e) {
e.preventDefault() // don't navigate
if (confirm('Are you sure you want to leave?')) {
window.location.href = href
}
}
return <a href={href} onClick={handleClick}>{children}</a>
}
// e.stopPropagation() — stops the event from bubbling to parent elements
function Modal({ onClose, children }) {
return (
<div className="overlay" onClick={onClose}>
<div
className="modal"
onClick={e => e.stopPropagation()} // click inside modal doesn't close it
>
{children}
</div>
</div>
)
}Accessing the Native Event
If you need a browser API that's on the native event but not exposed by SyntheticEvent, use e.nativeEvent:
function FileDropZone() {
function handleDrop(e) {
e.preventDefault()
// DataTransfer is on the native event
const files = e.nativeEvent.dataTransfer.files
processFiles(files)
}
return (
<div onDrop={handleDrop} onDragOver={e => e.preventDefault()}>
Drop files here
</div>
)
}Event Persistence (React 17 Change)
In React 16 and earlier, SyntheticEvent objects were pooled — React reused a single event object and cleared its properties after the handler returned. If you accessed e.target asynchronously (e.g., inside a setTimeout), the value would be null.
React 17 removed event pooling entirely. SyntheticEvent objects are now regular JavaScript objects that persist normally. You can safely access event properties asynchronously.
// React 17+: this works correctly
function handleChange(e) {
const value = e.target.value
setTimeout(() => {
console.log(value) // ✓ works fine
console.log(e.target) // ✓ also works (event is not pooled)
}, 1000)
}
// React 16 workaround (no longer needed):
function handleChangeLegacy(e) {
e.persist() // prevent event from being returned to pool
setTimeout(() => {
console.log(e.target.value) // would have been null without e.persist()
}, 1000)
}TypeScript Typing for Event Handlers
In TypeScript, React exports typed event interfaces for every event type. Use them to get full autocomplete and type safety:
import { ChangeEvent, FormEvent, KeyboardEvent, MouseEvent } from 'react'
function TypedForm() {
function handleInputChange(e: ChangeEvent<HTMLInputElement>) {
console.log(e.target.value) // string
}
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault()
}
function handleKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) {
if (e.key === 'Enter') { /* ... */ }
}
function handleButtonClick(e: MouseEvent<HTMLButtonElement>) {
console.log(e.clientX, e.clientY)
}
return (
<form onSubmit={handleSubmit}>
<input onChange={handleInputChange} />
<textarea onKeyDown={handleKeyDown} />
<button onClick={handleButtonClick} type="submit">Go</button>
</form>
)
}