Frontend Development 9 min read

Understanding React's Event System: Registration and Dispatch

This article explains how React registers event listeners on the root, extracts events from the fiber tree, synthesizes cross‑browser synthetic events, accumulates callbacks, and finally dispatches them, illustrating each step with code examples and diagrams.

ByteDance ADFE Team
ByteDance ADFE Team
ByteDance ADFE Team
Understanding React's Event System: Registration and Dispatch

React's event mechanism consists of two main steps: registering events on the root (or document) and dispatching them when a native browser event occurs. The programmer registers an event on a React node, the browser fires a native event, React captures it, maps the native node to its corresponding fiber node, and executes the associated callback.

During the initialization phase, JSX is transformed by Babel into a JSON representation called the React element tree, which is then converted into a fiber tree for diffing. While traversing the element tree depth‑first, event handlers are stored as properties on each node. After traversal, a real DOM tree is created, and each real DOM node is linked to its fiber node via an internalInstanceKey .

When building the real DOM, if a node has an event handler, React binds that event type to the root element or document . The following helper registers a bubbling listener:

function addEventBubbleListener(target, eventType, listener) {
  console.log(target, eventType, listener.name);
  target.addEventListener(eventType, listener, false);
  return listener;
}

React adds a single listener for each event type on the root, so every event bubbles up to that listener before being delegated to the appropriate component.

When a native event is triggered, React retrieves the native event object, obtains the native DOM node, and then uses the internalInstanceKey to find the associated fiber node. The code below shows how the native event target is resolved and how the closest fiber instance is fetched:

const nativeEventTarget = getEventTarget(nativeEvent);
const targetInst = getClosestInstanceFromNode(nativeEventTarget);
function getClosestInstanceFromNode(targetNode: Node) {
  let targetInst = (targetNode: any)[internalInstanceKey];
  if (targetInst) {
    // Don't return HostRoot or SuspenseComponent here
    return targetInst;
  }
}

React then synthesizes a cross‑browser React event. It wraps the native event, upgrades certain events (e.g., change , select ), and normalizes browser differences. The synthetic event is created as follows:

let EventConstructor;
switch (topLevelType) {
  case DOMTopLevelEventTypes.TOP_KEY_DOWN:
  case DOMTopLevelEventTypes.TOP_KEY_UP:
    EventConstructor = SyntheticKeyboardEvent;
    break;
  // other cases …
}
const event = new EventConstructor(
  reactName,
  null,
  nativeEvent,
  nativeEventTarget,
);

Next, React accumulates all listeners for the event by walking up the fiber tree, retrieving the callback from each component's props, and building a listeners array. Capture and bubble phases are handled by ordering the listeners appropriately:

function getListener() {
  const stateNode = inst.stateNode;
  if (stateNode === null) return null;
  const props = getFiberCurrentPropsFromNode(stateNode);
  if (props === null) return null;
  const listener = props[registrationName];
  return listener;
}

const listeners: Array
= [];
const bubbled = event._reactName;
const captured = bubbled !== null ? bubbled + 'Capture' : null;
if (bubbled !== null) {
  const bubbleListener = getListener(instance, bubbled);
  const entry = createDispatchListener(instance, bubbleListener, currentTarget);
  if (shouldEmulateTwoPhase) {
    listeners.unshift(entry);
  } else {
    listeners.push(entry);
  }
}

Finally, React processes the dispatch queue: each entry contains the synthetic event and its ordered listeners. It iterates through the queue, invoking each listener in the correct phase. If a listener calls stopPropagation , remaining listeners are skipped.

if (listeners.length !== 0) {
  dispatchQueue.push(createDispatchEntry(event, listeners));
}
function processDispatchQueue(dispatchQueue, eventSystemFlags) {
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  for (let i = 0; i < dispatchQueue.length; i++) {
    const {event, listeners} = dispatchQueue[i];
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
  }
}

The article concludes with references to the original React source files for functions such as addEventCaptureListener , attemptToDispatchEvent , and the event plugins used in the modern React event system.

frontendJavaScriptReactEvent SystemEvent DelegationSynthetic Events
ByteDance ADFE Team
Written by

ByteDance ADFE Team

Official account of ByteDance Advertising Frontend Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.