ReactThe Synthetic Event Object

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.

JSX
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 the onClick prop)

JSX
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: DIV
Reading 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.

JSX
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>
  )
}
Tip
For checkboxes, use `e.target.checked` (a boolean) instead of `e.target.value`. For select elements, `e.target.value` gives you the value of the selected `<option>`.
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

e.key

string

"Enter", "a", "ArrowUp"

Human-readable key name — prefer this

e.code

string

"KeyA", "Enter", "Space"

Physical key position, layout-independent

e.keyCode

number

13, 65, 32

Deprecated — use e.key instead

e.charCode

number

65

Deprecated — use e.key instead

e.shiftKey

boolean

true / false

Was Shift held?

e.ctrlKey

boolean

true / false

Was Ctrl held?

e.altKey

boolean

true / false

Was Alt/Option held?

e.metaKey

boolean

true / false

Was Cmd/Win held?

e.repeat

boolean

true / false

Is the key held down (repeating)?

JSX
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

JSX
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:

JSX
// 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:

JSX
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.

JSX
// 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)
}
Note
If you're maintaining older React code that calls `e.persist()`, you can safely remove those calls in React 17+. They're a no-op now (the method still exists to avoid breaking existing code, it just doesn't do anything).
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:

TSX
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>
  )
}