Synthetic Events

Difficulty: Beginner

React wraps the browser's native events in its own event system called SyntheticEvent. When you handle an event in React, you are not working with the browser's native event directly - you are working with a React-created wrapper object that has the same interface. SyntheticEvent normalizes event behavior across different browsers, ensuring your event handling code works consistently regardless of whether the user is on Chrome, Firefox, Safari, or Edge.

A SyntheticEvent object has the same properties and methods you would find on a native event: `target`, `currentTarget`, `type`, `preventDefault()`, `stopPropagation()`, and more. The `target` property refers to the DOM element that triggered the event. The `currentTarget` refers to the element the handler is attached to (they differ when events bubble). You can call `event.preventDefault()` to prevent default browser behavior (like form submission) and `event.stopPropagation()` to prevent the event from bubbling up.

Historically, React used event pooling for performance - the SyntheticEvent object was reused across events, and all properties were nullified after the handler returned. This meant you could not access event properties asynchronously (like in a setTimeout). Starting with React 17, event pooling was removed, so this is no longer an issue. However, it is still a common interview topic because it was a significant gotcha in older React versions.

If you need access to the underlying native browser event, you can use `event.nativeEvent`. This is rarely necessary but can be useful when working with browser APIs that require native events, such as certain drag-and-drop implementations or when integrating with non-React libraries. In general, stick with the SyntheticEvent interface for consistency.

React uses event delegation under the hood. Instead of attaching event listeners to every individual DOM node, React attaches a single listener to the root of your app (the root DOM container since React 17, previously the document). When an event occurs, React determines which component it belongs to and calls the appropriate handler. This delegation system is more memory-efficient than attaching individual listeners and is transparent to the developer.

Code examples

SyntheticEvent Properties

// Simulating a SyntheticEvent
function createSyntheticEvent(type, target, nativeEvent) {
  return {
    type,
    target,
    currentTarget: target,
    nativeEvent,
    defaultPrevented: false,
    propagationStopped: false,
    preventDefault() {
      this.defaultPrevented = true;
    },
    stopPropagation() {
      this.propagationStopped = true;
    }
  };
}

const event = createSyntheticEvent(
  'click',
  { tagName: 'BUTTON', id: 'submit-btn' },
  { /* native event */ }
);

console.log('Type: ' + event.type);
console.log('Target tag: ' + event.target.tagName);
console.log('Target id: ' + event.target.id);

event.preventDefault();
console.log('Default prevented: ' + event.defaultPrevented);

SyntheticEvents wrap native events with a consistent interface. They provide target, type, preventDefault(), stopPropagation(), and all standard event properties.

Event Target vs CurrentTarget

// target: the element that triggered the event
// currentTarget: the element the handler is attached to

function handleClick(event) {
  console.log('Clicked on: ' + event.target.tagName);
  console.log('Handler on: ' + event.currentTarget.tagName);
  console.log('Same element: ' + (event.target === event.currentTarget));
}

// Case 1: Click directly on the button
handleClick({
  target: { tagName: 'BUTTON' },
  currentTarget: { tagName: 'BUTTON' }
});

console.log('---');

// Case 2: Click on a span inside a div (event bubbles to div handler)
handleClick({
  target: { tagName: 'SPAN' },
  currentTarget: { tagName: 'DIV' }
});

event.target is what the user actually clicked. event.currentTarget is the element with the event handler. They differ when events bubble from child to parent.

preventDefault and stopPropagation

// preventDefault: stop the browser's default action
function handleSubmit(event) {
  event.preventDefault();
  console.log('Form submit prevented, handling with JS');
}

// stopPropagation: stop event from bubbling to parent
function handleInnerClick(event) {
  event.stopPropagation();
  console.log('Inner clicked, stopped propagation');
}

function handleOuterClick() {
  console.log('Outer clicked');
}

// Simulate form submit
const formEvent = { defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
handleSubmit(formEvent);
console.log('Default prevented: ' + formEvent.defaultPrevented);

console.log('---');

// Simulate nested click without stopPropagation
console.log('Without stopPropagation:');
handleOuterClick();

// With stopPropagation, outer would not fire
console.log('With stopPropagation:');
handleInnerClick({ stopPropagation() {} });

preventDefault() stops default browser behavior (form submission, link navigation). stopPropagation() prevents the event from reaching parent handlers.

Key points

Concepts covered

SyntheticEvent, event pooling, native events, event properties, cross-browser compatibility