facebook/reactMITd083ec1Report / request removal

Synthetic Event System

React DOM turns browser events into React-level events through listeners attached to root containers, an event registry, and a plugin-driven dispatch pipeline. The system keeps the native event as the entry point while using fibers to locate the React target and its ancestor listeners.

This design exists so one root-level native listener can route an event through React’s fiber tree, rather than requiring React’s event semantics to be implemented independently on every DOM element. The event system still installs direct listeners for events that do not delegate safely, such as media and other non-delegated events.

Sources: packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:432-459, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:589-703, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:236-250, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:346-374

Core concepts

Root container

A root container is the DOM event boundary where React installs delegated capture and bubble listeners for supported native events.

Sources: packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:432-459

Event registry

The event registry maps React registration names to native-event dependencies and collects the native event names that the root setup must consider.

Sources: packages/react-dom-bindings/src/events/EventRegistry.js:24-26, packages/react-dom-bindings/src/events/EventRegistry.js:14, packages/react-dom-bindings/src/events/EventRegistry.js:39-45

Synthetic event

A synthetic event is the React event object produced by event plugins from a native browser event; it normalizes browser behavior and carries React dispatch information.

Sources: packages/react-dom-bindings/src/events/SyntheticEvent.js:31-45, packages/react-dom-bindings/src/events/plugins/ChangeEventPlugin.js:48-65

Dispatch queue

A dispatch queue is the temporary collection of synthetic events and their ordered React listeners for one native event.

Sources: packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:325-344, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:313-323

How roots receive native events

When a root is initialized for event handling, listenToAllSupportedEvents uses a per-container listeningMarker so the same root is not initialized repeatedly. It iterates over allNativeEvents, installs delegated bubble listeners for events outside nonDelegatedEvents, and installs capture listeners for the supported events. selectionchange is handled separately on the owner document because it does not bubble.

The root listener is not the final React handler. addTrappedEventListener first creates a wrapper with createEventListenerWrapperWithPriority, then attaches it in capture or bubble mode, optionally using passive behavior for touchstart, touchmove, and wheel.

React chooses the wrapper from getEventPriority: discrete events use dispatchDiscreteEvent, continuous events use dispatchContinuousEvent, and default events use dispatchEvent. This preserves event priority before the plugin pipeline begins.

From root setup to dispatch — How does a root listener reach the plugin pipeline?

Evidence

The sequence shows why delegation works: root setup creates the native entry point, while the event wrapper carries the native event name, flags, and target container into React’s dispatcher.

Sources: packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:432-459, packages/react-dom-bindings/src/events/EventRegistry.js:14, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:236-250, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:461-558, packages/react-dom-bindings/src/events/ReactDOMEventListener.js:94-119

How the registry decides delegation

The registry has three complementary structures:

Registry structurePlain-language role
allNativeEventsSet of native event names that the root setup scans.
registrationNameDependenciesMaps a React registration name to the native events that can produce it.
possibleRegistrationNamesDevelopment-only lookup from lower-cased names to their registered spelling.

These structures are populated by registerDirectEvent; registerTwoPhaseEvent registers both the normal name and its Capture form, and adds every dependency to allNativeEvents.

Delegation is therefore decided in two stages. First, a native event must be present in allNativeEvents; second, listenToAllSupportedEvents checks whether nonDelegatedEvents contains it before adding a root bubble listener. Capture setup still occurs through listenToNativeEvent, while non-delegated elements receive their own trapped listener through listenToNonDelegatedEvent.

The non-delegated set includes events such as scroll, load, toggle, and the media events listed by mediaEventTypes. For example, audio elements receive direct listeners for every media event, while image-like elements receive direct error and load listeners.

React deduplicates these direct listeners with a listener set keyed by the event name and phase, such as the value produced by getListenerSetKey.

Sources: packages/react-dom-bindings/src/events/EventRegistry.js:14, packages/react-dom-bindings/src/events/EventRegistry.js:24-26, packages/react-dom-bindings/src/events/EventRegistry.js:34-36, packages/react-dom-bindings/src/events/EventRegistry.js:39-45, packages/react-dom-bindings/src/events/EventRegistry.js:47-75, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:432-459, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:236-250, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:346-374, packages/react-dom-bindings/src/client/ReactDOMComponent.js:1396-1409, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:1005-1010

How a native event reaches the right fiber

dispatchEvent first checks whether the event system is enabled. It then calls findInstanceBlockingEvent, which maps the native event target to a DOM-associated fiber through findInstanceBlockingTarget and getClosestInstanceFromNode. The result is stored in return_targetInst when dispatch can proceed.

If the event is blocked by a Suspense or Activity instance, the system can defer it for replay; if the root is dehydrated, it returns the container as the blocking target. If the event is not blocked, dispatchEvent calls dispatchEventForPluginEventSystem.

The plugin dispatcher walks upward from the target fiber until it finds a matching root container. This root-aware traversal matters for multiple roots and portals: the dispatcher compares containers with isMatchingRootContainer, and may switch to a corresponding host fiber in another tree before dispatching.

dispatchEventsForPlugins obtains the native target, creates an empty dispatch queue, calls extractEvents, and then processes the resulting queue.

Native event to React listeners — How does a native event become a dispatched React event?

Evidence

The plugin pipeline calls SimpleEventPlugin for basic events and conditionally calls polyfill plugins including EnterLeaveEventPlugin, ChangeEventPlugin, SelectEventPlugin, BeforeInputEventPlugin, and FormActionEventPlugin; the scroll-end plugin is separately feature-gated.

For a change event, ChangeEventPlugin accumulates two-phase listeners and constructs a SyntheticEvent with the native event and target before adding it to the dispatch queue.

Listener accumulation walks from the target fiber toward the root. Standard React on* listeners are found with getListener, wrapped with createDispatchListener, and stored with their fiber and current target. The two-phase path places capture listeners in front and bubble listeners after them, stopping if the fiber path never reaches a HostRoot.

Finally, processDispatchQueue selects capture or bubble order and calls processDispatchQueueItemsInOrder. That function stops when propagation has been stopped and invokes executeDispatch, which sets currentTarget, calls the listener, reports errors globally, and clears currentTarget.

Sources: packages/react-dom-bindings/src/events/ReactDOMEventListener.js:157-235, packages/react-dom-bindings/src/events/ReactDOMEventListener.js:237-242, packages/react-dom-bindings/src/events/ReactDOMEventListener.js:248-310, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:589-703, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:577-587, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:325-344, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:102-203, packages/react-dom-bindings/src/events/plugins/ChangeEventPlugin.js:48-65, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:717-829, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:839-879, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:313-323, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:266-311, packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:252-264

How it connects

The event system depends on fibers to identify mounted targets and traverse root boundaries, so it connects directly to Fiber Architecture and ReactDOM Host Config.

Its dispatch work runs inside batchedUpdates, connecting native events to Work Loop & Scheduling and Lanes & Priority.

Root attachment is part of the DOM renderer boundary described by Commit Phase, while blocked and replayable events intersect with Suspense & Error Boundaries and Hydration.

Sources: packages/react-dom-bindings/src/events/DOMPluginEventSystem.js:589-703, packages/react-dom-bindings/src/events/ReactDOMEventListener.js:248-310, packages/react-dom-bindings/src/events/ReactDOMEventListener.js:94-119, packages/react-dom-bindings/src/events/ReactDOMEventListener.js:157-235

Key takeaways

  • React installs delegated listeners at root containers and uses fibers to reconstruct the React listener path.
  • The event registry turns registration-name dependencies into the native event set used by root setup.
  • Non-delegated events receive direct element listeners, with per-event and per-phase deduplication.
  • Plugins extract native events into synthetic events and dispatch queues.
  • Dispatch follows React’s capture/bubble ordering and honors propagation stopping.

Want this for your repos?

Try Angada AI Wiki