diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 1de7794bbece9..c4a65ac5ba37d 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -11,6 +11,7 @@ import type { ReactContext, ReactProviderType, ReactEventResponder, + ReactEventResponderListener, } from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {Hook} from 'react-reconciler/src/ReactFiberHooks'; @@ -25,8 +26,6 @@ import { ForwardRef, } from 'shared/ReactWorkTags'; -const emptyObject = {}; - type CurrentDispatcherRef = typeof ReactSharedInternals.ReactCurrentDispatcher; // Used to track hooks called during a render @@ -221,17 +220,20 @@ function useMemo( return value; } -function useListener( +function useResponder( responder: ReactEventResponder, - hookProps: ?Object, -): void { - const listenerProps = hookProps || emptyObject; + listenerProps: Object, +): ReactEventResponderListener { // Don't put the actual event responder object in, just its displayName const value = { responder: responder.displayName || 'EventResponder', props: listenerProps, }; - hookLog.push({primitive: 'Listener', stackError: new Error(), value}); + hookLog.push({primitive: 'Responder', stackError: new Error(), value}); + return { + responder, + props: listenerProps, + }; } const Dispatcher: DispatcherType = { @@ -246,7 +248,7 @@ const Dispatcher: DispatcherType = { useReducer, useRef, useState, - useListener, + useResponder, }; // Inspect diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js index ac838e2a24492..b449e0120d5fe 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js @@ -22,19 +22,21 @@ describe('ReactHooksInspection', () => { ReactDebugTools = require('react-debug-tools'); }); - it('should inspect a simple useListener hook', () => { + it('should inspect a simple useResponder hook', () => { const TestResponder = React.unstable_createResponder('TestResponder', {}); function Foo(props) { - React.unstable_useListener(TestResponder, {preventDefault: false}); - return
}>Hello world
; + const listener = React.unstable_useResponder(TestResponder, { + preventDefault: false, + }); + return
Hello world
; } let tree = ReactDebugTools.inspectHooks(Foo, {}); expect(tree).toEqual([ { isStateEditable: false, id: 0, - name: 'Listener', + name: 'Responder', value: {props: {preventDefault: false}, responder: 'TestResponder'}, subHooks: [], }, diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index 55db92a52f3ce..c2c3b791867a3 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -98,7 +98,7 @@ const AUTOFOCUS = 'autoFocus'; const CHILDREN = 'children'; const STYLE = 'style'; const HTML = '__html'; -const RESPONDERS = 'responders'; +const LISTENERS = 'listeners'; const {html: HTML_NAMESPACE} = Namespaces; @@ -341,7 +341,7 @@ function setInitialDOMProperties( setTextContent(domElement, '' + nextProp); } } else if ( - (enableFlareAPI && propKey === RESPONDERS) || + (enableFlareAPI && propKey === LISTENERS) || propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING ) { @@ -698,7 +698,7 @@ export function diffProperties( } else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) { // Noop. This is handled by the clear text mechanism. } else if ( - (enableFlareAPI && propKey === RESPONDERS) || + (enableFlareAPI && propKey === LISTENERS) || propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING ) { @@ -790,7 +790,7 @@ export function diffProperties( (updatePayload = updatePayload || []).push(propKey, '' + nextProp); } } else if ( - (enableFlareAPI && propKey === RESPONDERS) || + (enableFlareAPI && propKey === LISTENERS) || propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING ) { @@ -1045,7 +1045,7 @@ export function diffHydratedProperties( if (suppressHydrationWarning) { // Don't bother comparing. We're ignoring all these warnings. } else if ( - (enableFlareAPI && propKey === RESPONDERS) || + (enableFlareAPI && propKey === LISTENERS) || propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING || // Controlled attributes are not validated diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js index 4be4fa81399ab..f034670465b0e 100644 --- a/packages/react-dom/src/events/DOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -12,12 +12,7 @@ import { PASSIVE_NOT_SUPPORTED, } from 'events/EventSystemFlags'; import type {AnyNativeEvent} from 'events/PluginModuleType'; -import { - HostComponent, - FunctionComponent, - MemoComponent, - ForwardRef, -} from 'shared/ReactWorkTags'; +import {HostComponent} from 'shared/ReactWorkTags'; import type {EventPriority} from 'shared/ReactTypes'; import type { ReactDOMEventResponder, @@ -67,7 +62,7 @@ export function setListenToResponderEventTypes( } type EventQueueItem = {| - listeners: Array<(val: any) => void>, + listener: (val: any) => void, value: any, |}; type EventQueue = Array; @@ -103,8 +98,8 @@ let currentDocument: null | Document = null; const eventResponderContext: ReactDOMResponderContext = { dispatchEvent( - eventProp: string, eventValue: any, + eventListener: any => void, eventPriority: EventPriority, ): void { validateResponderContext(); @@ -112,15 +107,9 @@ const eventResponderContext: ReactDOMResponderContext = { if (eventPriority < currentEventQueuePriority) { currentEventQueuePriority = eventPriority; } - const responderInstance = ((currentInstance: any): ReactDOMEventResponderInstance); - const target = responderInstance.fiber; - const responder = responderInstance.responder; - const listeners = collectListeners(eventProp, responder, target); - if (listeners.length !== 0) { - ((currentEventQueue: any): EventQueue).push( - createEventQueueItem(eventValue, listeners), - ); - } + ((currentEventQueue: any): EventQueue).push( + createEventQueueItem(eventValue, eventListener), + ); }, isTargetWithinResponder(target: Element | Document): boolean { validateResponderContext(); @@ -392,11 +381,11 @@ function collectFocusableElements( function createEventQueueItem( value: any, - listeners: Array<(val: any) => void>, + listener: (val: any) => void, ): EventQueueItem { return { value, - listeners, + listener, }; } @@ -519,70 +508,11 @@ function createDOMResponderEvent( }; } -function collectListeners( - eventProp: string, - eventResponder: ReactDOMEventResponder, - target: Fiber, -): Array<(any) => void> { - const eventListeners = []; - let node = target.return; - nodeTraversal: while (node !== null) { - switch (node.tag) { - case HostComponent: { - const dependencies = node.dependencies; - - if (dependencies !== null) { - const respondersMap = dependencies.responders; - - if (respondersMap !== null && respondersMap.has(eventResponder)) { - break nodeTraversal; - } - } - break; - } - case FunctionComponent: - case MemoComponent: - case ForwardRef: { - const dependencies = node.dependencies; - - if (dependencies !== null) { - const listeners = dependencies.listeners; - - if (listeners !== null) { - for ( - let s = 0, listenersLength = listeners.length; - s < listenersLength; - s++ - ) { - const listener = listeners[s]; - const {responder, props} = listener; - const listenerFunc = props[eventProp]; - - if ( - responder === eventResponder && - typeof listenerFunc === 'function' - ) { - eventListeners.push(listenerFunc); - } - } - } - } - } - } - node = node.return; - } - return eventListeners; -} - function processEvents(eventQueue: EventQueue): void { for (let i = 0, length = eventQueue.length; i < length; i++) { - const {value, listeners} = eventQueue[i]; - for (let s = 0, length2 = listeners.length; s < length2; s++) { - const listener = listeners[s]; - const type = - typeof value === 'object' && value !== null ? value.type : ''; - invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, value); - } + const {value, listener} = eventQueue[i]; + const type = typeof value === 'object' && value !== null ? value.type : ''; + invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, value); } } @@ -663,6 +593,7 @@ function traverseAndHandleEventResponderInstances( // - Bubble target responder phase // - Root responder phase + const visitedResponders = new Set(); const responderEvent = createDOMResponderEvent( topLevelType, nativeEvent, @@ -670,7 +601,6 @@ function traverseAndHandleEventResponderInstances( isPassiveEvent, isPassiveSupported, ); - const visitedResponders = new Set(); let node = targetFiber; while (node !== null) { const {dependencies, tag} = node; @@ -687,8 +617,8 @@ function traverseAndHandleEventResponderInstances( !visitedResponders.has(responder) && validateResponderTargetEventTypes(eventType, responder) ) { - const onEvent = responder.onEvent; visitedResponders.add(responder); + const onEvent = responder.onEvent; if (onEvent !== null) { currentInstance = responderInstance; responderEvent.responderTarget = ((target: any): diff --git a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js index 7efbeaa5f1a5b..6a4894de1736b 100644 --- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js @@ -87,19 +87,28 @@ describe('DOMEventResponderSystem', () => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableFlareAPI = true; + React = require('react'); ReactTestRenderer = require('react-test-renderer'); const TestResponder = createEventResponder({}); - const renderer = ReactTestRenderer.create( -
}>Hello world
, - ); + + function Test() { + const listener = React.unstable_useResponder(TestResponder, {}); + + return
Hello world
; + } + const renderer = ReactTestRenderer.create(); expect(renderer).toMatchRenderedOutput(
Hello world
); }); it('can render correctly with the ReactDOMServer', () => { const TestResponder = createEventResponder({}); - const output = ReactDOMServer.renderToString( -
}>Hello world
, - ); + + function Test() { + const listener = React.unstable_useResponder(TestResponder, {}); + + return
Hello world
; + } + const output = ReactDOMServer.renderToString(); expect(output).toBe(`
Hello world
`); }); @@ -121,11 +130,15 @@ describe('DOMEventResponderSystem', () => { }, }); - const Test = () => ( - - ); + function Test() { + const listener = React.unstable_useResponder(TestResponder, {}); + + return ( + + ); + } ReactDOM.render(, container); expect(container.innerHTML).toBe(''); @@ -177,11 +190,15 @@ describe('DOMEventResponderSystem', () => { }, }); - const Test = () => ( - - ); + function Test() { + const listener = React.unstable_useResponder(TestResponder, {}); + + return ( + + ); + } ReactDOM.render(, container); @@ -217,15 +234,23 @@ describe('DOMEventResponderSystem', () => { }, }); - let Test = () => ( - - ); + function Test() { + const listener = React.unstable_useResponder(TestResponder, {}); + const listener2 = React.unstable_useResponder(TestResponder, {}); - ReactDOM.render(, container); + return ( + + ); + } + + expect(() => { + ReactDOM.render(, container); + }).toWarnDev( + 'Duplicate event responder "TestEventResponder" found in event listeners. ' + + 'Event listeners passed to elements cannot use the same event responder more than once.', + ); // Clicking the button should trigger the event responder onEvent() let buttonElement = buttonRef.current; @@ -244,15 +269,19 @@ describe('DOMEventResponderSystem', () => { eventLog = []; - Test = () => ( -
}> - -
- ); + function Test2() { + const listener = React.unstable_useResponder(TestResponder, {}); - ReactDOM.render(, container); + return ( +
+ +
+ ); + } + + ReactDOM.render(, container); // Clicking the button should trigger the event responder onEvent() buttonElement = buttonRef.current; @@ -288,13 +317,16 @@ describe('DOMEventResponderSystem', () => { }, }); - let Test = () => ( - - ); + function Test() { + const listener = React.unstable_useResponder(TestResponderA, {}); + const listener2 = React.unstable_useResponder(TestResponderB, {}); + + return ( + + ); + } ReactDOM.render(, container); @@ -306,15 +338,20 @@ describe('DOMEventResponderSystem', () => { eventLog = []; - Test = () => ( -
}> - -
- ); + function Test2() { + const listener = React.unstable_useResponder(TestResponderA, {}); + const listener2 = React.unstable_useResponder(TestResponderB, {}); - ReactDOM.render(, container); + return ( +
+ +
+ ); + } + + ReactDOM.render(, container); // Clicking the button should trigger the event responder onEvent() buttonElement = buttonRef.current; @@ -323,7 +360,7 @@ describe('DOMEventResponderSystem', () => { expect(eventLog).toEqual(['B [bubble]', 'A [bubble]']); }); - it('nested event responders should fire in the correct order', () => { + it('nested event responders should fire in the correct order #2', () => { let eventLog = []; const buttonRef = React.createRef(); @@ -334,13 +371,17 @@ describe('DOMEventResponderSystem', () => { }, }); - const Test = () => ( -
}> - -
- ); + const Test = () => { + const listener = React.unstable_useResponder(TestResponder, {name: 'A'}); + const listener2 = React.unstable_useResponder(TestResponder, {name: 'B'}); + return ( +
+ +
+ ); + }; ReactDOM.render(, container); @@ -364,7 +405,11 @@ describe('DOMEventResponderSystem', () => { phase: 'bubble', timeStamp: context.getTimeStamp(), }; - context.dispatchEvent('onMagicClick', syntheticEvent, DiscreteEvent); + context.dispatchEvent( + syntheticEvent, + props.onMagicClick, + DiscreteEvent, + ); }, }); @@ -373,12 +418,12 @@ describe('DOMEventResponderSystem', () => { } const Test = () => { - React.unstable_useListener(TestResponder, { + const listener = React.unstable_useResponder(TestResponder, { onMagicClick: handleMagicEvent, }); return ( - ); @@ -404,7 +449,7 @@ describe('DOMEventResponderSystem', () => { phase, timeStamp: context.getTimeStamp(), }; - context.dispatchEvent('onPress', pressEvent, DiscreteEvent); + context.dispatchEvent(pressEvent, props.onPress, DiscreteEvent); context.setTimeout(() => { const longPressEvent = { @@ -413,7 +458,7 @@ describe('DOMEventResponderSystem', () => { phase, timeStamp: context.getTimeStamp(), }; - context.dispatchEvent('onLongPress', longPressEvent, DiscreteEvent); + context.dispatchEvent(longPressEvent, props.onLongPress, DiscreteEvent); const longPressChangeEvent = { target: event.target, @@ -422,8 +467,8 @@ describe('DOMEventResponderSystem', () => { timeStamp: context.getTimeStamp(), }; context.dispatchEvent( - 'onLongPressChange', longPressChangeEvent, + props.onLongPressChange, DiscreteEvent, ); }, 500); @@ -441,14 +486,14 @@ describe('DOMEventResponderSystem', () => { } const Test = () => { - React.unstable_useListener(TestResponder, { + const listener = React.unstable_useResponder(TestResponder, { onPress: e => log('press ' + e.phase), onLongPress: e => log('longpress ' + e.phase), onLongPressChange: e => log('longpresschange ' + e.phase), }); return ( - ); @@ -485,16 +530,19 @@ describe('DOMEventResponderSystem', () => { }, }); - ReactDOM.render( - - ); + const Test = () => { + const listener = React.unstable_useResponder(TestResponder, {}); + return ; + }; ReactDOM.render(, container); expect(container.innerHTML).toBe(''); @@ -647,13 +715,18 @@ describe('DOMEventResponderSystem', () => { }, }); - const Test = () => ( -
}> - -
- ); + const Test = () => { + const listener = React.unstable_useResponder(TestResponderA, {}); + const listener2 = React.unstable_useResponder(TestResponderB, {}); + + return ( +
+ +
+ ); + }; ReactDOM.render(, container); @@ -706,11 +779,16 @@ describe('DOMEventResponderSystem', () => { }, }); - const Test = () => ( -
}> - -
- ); + const Test = () => { + const listener = React.unstable_useResponder(TestResponderA, {}); + const listener2 = React.unstable_useResponder(TestResponderB, {}); + + return ( +
+ +
+ ); + }; ReactDOM.render(, container); @@ -744,17 +822,17 @@ describe('DOMEventResponderSystem', () => { type: 'click', timeStamp: context.getTimeStamp(), }; - context.dispatchEvent('onClick', syntheticEvent, DiscreteEvent); + context.dispatchEvent(syntheticEvent, props.onClick, DiscreteEvent); }, }); let handler; const Test = () => { - React.unstable_useListener(TestResponder, { + const listener = React.unstable_useResponder(TestResponder, { onClick: handler, }); - return ; + return ; }; expect(() => { handler = event => { @@ -819,7 +897,7 @@ describe('DOMEventResponderSystem', () => { expect(container.innerHTML).toBe(''); }); - it('should work with event listener hooks', () => { + it('should work with event responder hooks', () => { const buttonRef = React.createRef(); const eventLogs = []; const TestResponder = createEventResponder({ @@ -830,16 +908,16 @@ describe('DOMEventResponderSystem', () => { type: 'foo', timeStamp: context.getTimeStamp(), }; - context.dispatchEvent('onFoo', fooEvent, DiscreteEvent); + context.dispatchEvent(fooEvent, props.onFoo, DiscreteEvent); }, }); const Test = () => { - React.unstable_useListener(TestResponder, { + const listener = React.unstable_useResponder(TestResponder, { onFoo: e => eventLogs.push('hook'), }); - return