@samitouri / QOS-React-2 / commits / 720db4cbe6

[Flare] Add useEvent hook implementation (#15927)

* [Flare] Add useEvent hook implementation Validate hooks have decendent event components Few fixes and displayName changes Fix more responder bugs Update error codes * Add another test * Address feedback

Dominic Gannaway committed Jun 21, 2019 at 03:12 UTC 720db4cbe675e80820ec81abab499492309b9252
29 files changed +421 -116
packages/react-debug-tools/src/ReactDebugHooks.js
+5
@@ -215,6 +215,10 @@ function useMemo<T>(
215 return value;
216 }
217
218 +function useEvent() {
219 + throw new Error('TODO: not yet implemented');
220 +}
221 +
222 const Dispatcher: DispatcherType = {
223 readContext,
224 useCallback,
@@ -227,6 +231,7 @@ const Dispatcher: DispatcherType = {
231 useReducer,
232 useRef,
233 useState,
234 + useEvent,
235 };
236
237 // Inspect
packages/react-dom/src/events/DOMEventResponderSystem.js
+137 -44
@@ -16,6 +16,7 @@ import {
16 EventComponent,
17 EventTarget as EventTargetWorkTag,
18 HostComponent,
19 + FunctionComponent,
20 } from 'shared/ReactWorkTags';
21 import type {
22 ReactEventComponentInstance,
@@ -117,6 +118,7 @@ let currentTimers = new Map();
118 let currentInstance: null | ReactEventComponentInstance = null;
119 let currentEventQueue: null | EventQueue = null;
120 let currentTimerIDCounter = 0;
121 +let currentDocument: null | Document = null;
122
123 const eventResponderContext: ReactDOMResponderContext = {
124 dispatchEvent(
@@ -209,15 +211,41 @@ const eventResponderContext: ReactDOMResponderContext = {
211 }
212 return false;
213 },
212 - isTargetWithinEventComponent,
214 + isTargetWithinEventComponent(target: Element | Document): boolean {
215 + validateResponderContext();
216 + if (target != null) {
217 + let fiber = getClosestInstanceFromNode(target);
218 + const currentFiber = ((currentInstance: any): ReactEventComponentInstance)
219 + .currentFiber;
220 +
221 + while (fiber !== null) {
222 + if (fiber.tag === EventComponent) {
223 + // Switch to the current fiber tree
224 + fiber = fiber.stateNode.currentFiber;
225 + }
226 + if (fiber === currentFiber || fiber.stateNode === currentInstance) {
227 + return true;
228 + }
229 + fiber = fiber.return;
230 + }
231 + }
232 + return false;
233 + },
234 isTargetWithinEventResponderScope(target: Element | Document): boolean {
235 validateResponderContext();
215 - const responder = ((currentInstance: any): ReactEventComponentInstance)
216 - .responder;
236 + const componentInstance = ((currentInstance: any): ReactEventComponentInstance);
237 + const responder = componentInstance.responder;
238 +
239 if (target != null) {
240 let fiber = getClosestInstanceFromNode(target);
241 + const currentFiber = ((currentInstance: any): ReactEventComponentInstance)
242 + .currentFiber;
243 while (fiber !== null) {
220 - if (fiber.stateNode === currentInstance) {
244 + if (fiber.tag === EventComponent) {
245 + // Switch to the current fiber tree
246 + fiber = fiber.stateNode.currentFiber;
247 + }
248 + if (fiber === currentFiber || fiber.stateNode === currentInstance) {
249 return true;
250 }
251 if (
@@ -376,9 +404,15 @@ const eventResponderContext: ReactDOMResponderContext = {
404 const target = event.target;
405 let fiber = getClosestInstanceFromNode(target);
406 let hostComponent = target;
407 + const currentResponder = ((currentInstance: any): ReactEventComponentInstance)
408 + .responder;
409
410 while (fiber !== null) {
381 - if (fiber.stateNode === currentInstance) {
411 + const stateNode = fiber.stateNode;
412 + if (
413 + fiber.tag === EventComponent &&
414 + (stateNode === null || stateNode.responder === currentResponder)
415 + ) {
416 break;
417 }
418 if (fiber.tag === HostComponent) {
@@ -399,8 +433,16 @@ const eventResponderContext: ReactDOMResponderContext = {
433 ): boolean {
434 validateResponderContext();
435 let fiber = getClosestInstanceFromNode(target);
436 + const currentResponder = ((currentInstance: any): ReactEventComponentInstance)
437 + .responder;
438 +
439 while (fiber !== null) {
403 - if (!deep && fiber.stateNode === currentInstance) {
440 + const stateNode = fiber.stateNode;
441 + if (
442 + !deep &&
443 + (fiber.tag === EventComponent &&
444 + (stateNode === null || stateNode.responder === currentResponder))
445 + ) {
446 return false;
447 }
448 if (fiber.tag === HostComponent && fiber.type === elementType) {
@@ -443,24 +485,8 @@ function collectFocusableElements(
485 }
486 }
487
446 -function isTargetWithinEventComponent(target: Element | Document): boolean {
447 - validateResponderContext();
448 - if (target != null) {
449 - let fiber = getClosestInstanceFromNode(target);
450 - while (fiber !== null) {
451 - if (fiber.stateNode === currentInstance) {
452 - return true;
453 - }
454 - fiber = fiber.return;
455 - }
456 - }
457 - return false;
458 -}
459 -
488 function getActiveDocument(): Document {
461 - const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
462 - const rootElement = ((eventComponentInstance.rootInstance: any): Element);
463 - return rootElement.ownerDocument;
489 + return ((currentDocument: any): Document);
490 }
491
492 function releaseOwnershipForEventComponentInstance(
@@ -651,23 +677,60 @@ function getDOMTargetEventTypesSet(
677 return cachedSet;
678 }
679
680 +function storeTargetEventResponderInstance(
681 + listeningName: string,
682 + eventComponentInstance: ReactEventComponentInstance,
683 + eventResponderInstances: Array<ReactEventComponentInstance>,
684 + eventComponentResponders: null | Set<ReactDOMEventResponder>,
685 +): void {
686 + const responder = eventComponentInstance.responder;
687 + const targetEventTypes = responder.targetEventTypes;
688 + // Validate the target event type exists on the responder
689 + if (targetEventTypes !== undefined) {
690 + const targetEventTypesSet = getDOMTargetEventTypesSet(targetEventTypes);
691 + if (targetEventTypesSet.has(listeningName)) {
692 + eventResponderInstances.push(eventComponentInstance);
693 + if (eventComponentResponders !== null) {
694 + eventComponentResponders.add(responder);
695 + }
696 + }
697 + }
698 +}
699 +
700 function getTargetEventResponderInstances(
701 listeningName: string,
702 targetFiber: null | Fiber,
703 ): Array<ReactEventComponentInstance> {
704 + // We use this to know if we should check add hooks. If there are
705 + // no event targets, then we don't add the hook forms.
706 + const eventComponentResponders = new Set();
707 const eventResponderInstances = [];
708 let node = targetFiber;
709 while (node !== null) {
710 // Traverse up the fiber tree till we find event component fibers.
662 - if (node.tag === EventComponent) {
711 + const tag = node.tag;
712 + const events = node.dependencies.events;
713 +
714 + if (tag === EventComponent) {
715 const eventComponentInstance = node.stateNode;
664 - const responder = eventComponentInstance.responder;
665 - const targetEventTypes = responder.targetEventTypes;
666 - // Validate the target event type exists on the responder
667 - if (targetEventTypes !== undefined) {
668 - const targetEventTypesSet = getDOMTargetEventTypesSet(targetEventTypes);
669 - if (targetEventTypesSet.has(listeningName)) {
670 - eventResponderInstances.push(eventComponentInstance);
716 + // Switch to the current fiber tree
717 + node = eventComponentInstance.currentFiber;
718 + storeTargetEventResponderInstance(
719 + listeningName,
720 + eventComponentInstance,
721 + eventResponderInstances,
722 + eventComponentResponders,
723 + );
724 + } else if (tag === FunctionComponent && events !== null) {
725 + for (let i = 0; i < events.length; i++) {
726 + const eventComponentInstance = events[i];
727 + if (eventComponentResponders.has(eventComponentInstance.responder)) {
728 + storeTargetEventResponderInstance(
729 + listeningName,
730 + eventComponentInstance,
731 + eventResponderInstances,
732 + null,
733 + );
734 }
735 }
736 }
@@ -698,8 +761,9 @@ function shouldSkipEventComponent(
761 eventResponderInstance: ReactEventComponentInstance,
762 responder: ReactDOMEventResponder,
763 propagatedEventResponders: null | Set<ReactDOMEventResponder>,
764 + localPropagation: boolean,
765 ): boolean {
702 - if (propagatedEventResponders !== null) {
766 + if (propagatedEventResponders !== null && localPropagation) {
767 if (propagatedEventResponders.has(responder)) {
768 return true;
769 }
@@ -764,7 +828,12 @@ function traverseAndHandleEventResponderInstances(
828 // Capture target phase
829 for (i = length; i-- > 0; ) {
830 const targetEventResponderInstance = targetEventResponderInstances[i];
767 - const {responder, props, state} = targetEventResponderInstance;
831 + const {
832 + localPropagation,
833 + props,
834 + responder,
835 + state,
836 + } = targetEventResponderInstance;
837 const eventListener = responder.onEventCapture;
838 if (eventListener !== undefined) {
839 if (
@@ -772,16 +841,19 @@ function traverseAndHandleEventResponderInstances(
841 targetEventResponderInstance,
842 responder,
843 propagatedEventResponders,
844 + localPropagation,
845 )
846 ) {
847 continue;
848 }
849 currentInstance = targetEventResponderInstance;
850 eventListener(responderEvent, eventResponderContext, props, state);
781 - checkForLocalPropagationContinuation(
782 - responder,
783 - propagatedEventResponders,
784 - );
851 + if (localPropagation) {
852 + checkForLocalPropagationContinuation(
853 + responder,
854 + propagatedEventResponders,
855 + );
856 + }
857 }
858 }
859 // We clean propagated event responders between phases.
@@ -789,7 +861,12 @@ function traverseAndHandleEventResponderInstances(
861 // Bubble target phase
862 for (i = 0; i < length; i++) {
863 const targetEventResponderInstance = targetEventResponderInstances[i];
792 - const {responder, props, state} = targetEventResponderInstance;
864 + const {
865 + localPropagation,
866 + props,
867 + responder,
868 + state,
869 + } = targetEventResponderInstance;
870 const eventListener = responder.onEvent;
871 if (eventListener !== undefined) {
872 if (
@@ -797,16 +874,19 @@ function traverseAndHandleEventResponderInstances(
874 targetEventResponderInstance,
875 responder,
876 propagatedEventResponders,
877 + localPropagation,
878 )
879 ) {
880 continue;
881 }
882 currentInstance = targetEventResponderInstance;
883 eventListener(responderEvent, eventResponderContext, props, state);
806 - checkForLocalPropagationContinuation(
807 - responder,
808 - propagatedEventResponders,
809 - );
884 + if (localPropagation) {
885 + checkForLocalPropagationContinuation(
886 + responder,
887 + propagatedEventResponders,
888 + );
889 + }
890 }
891 }
892 }
@@ -818,11 +898,21 @@ function traverseAndHandleEventResponderInstances(
898 if (length > 0) {
899 for (i = 0; i < length; i++) {
900 const rootEventResponderInstance = rootEventResponderInstances[i];
821 - const {responder, props, state} = rootEventResponderInstance;
901 + const {
902 + localPropagation,
903 + props,
904 + responder,
905 + state,
906 + } = rootEventResponderInstance;
907 const eventListener = responder.onRootEvent;
908 if (eventListener !== undefined) {
909 if (
825 - shouldSkipEventComponent(rootEventResponderInstance, responder, null)
910 + shouldSkipEventComponent(
911 + rootEventResponderInstance,
912 + responder,
913 + null,
914 + localPropagation,
915 + )
916 ) {
917 continue;
918 }
@@ -936,8 +1026,10 @@ export function dispatchEventForResponderEventSystem(
1026 const previousInstance = currentInstance;
1027 const previousTimers = currentTimers;
1028 const previousTimeStamp = currentTimeStamp;
1029 + const previousDocument = currentDocument;
1030 currentTimers = null;
1031 currentEventQueue = createEventQueue();
1032 + currentDocument = (nativeEventTarget: any).ownerDocument;
1033 // We might want to control timeStamp another way here
1034 currentTimeStamp = (nativeEvent: any).timeStamp;
1035 try {
@@ -954,6 +1046,7 @@ export function dispatchEventForResponderEventSystem(
1046 currentInstance = previousInstance;
1047 currentEventQueue = previousEventQueue;
1048 currentTimeStamp = previousTimeStamp;
1049 + currentDocument = previousDocument;
1050 }
1051 }
1052 }
packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
+83 -15
@@ -28,8 +28,10 @@ function createReactEventComponent({
28 onUnmount,
29 onOwnershipChange,
30 allowMultipleHostChildren,
31 + allowEventHooks,
32 }) {
33 const testEventResponder = {
34 + displayName: 'TestEventComponent',
35 targetEventTypes,
36 rootEventTypes,
37 createInitialState,
@@ -40,14 +42,23 @@ function createReactEventComponent({
42 onUnmount,
43 onOwnershipChange,
44 allowMultipleHostChildren: allowMultipleHostChildren || false,
45 + allowEventHooks: allowEventHooks || true,
46 };
47
45 - return ReactDOM.unstable_createEvent(
46 - testEventResponder,
47 - 'TestEventComponent',
48 - );
48 + return ReactDOM.unstable_createEvent(testEventResponder);
49 }
50
51 +const createEvent = (type, data) => {
52 + const event = document.createEvent('CustomEvent');
53 + event.initCustomEvent(type, true, true);
54 + if (data != null) {
55 + Object.entries(data).forEach(([key, value]) => {
56 + event[key] = value;
57 + });
58 + }
59 + return event;
60 +};
61 +
62 function dispatchEvent(element, type) {
63 const event = document.createEvent('Event');
64 event.initEvent(type, true, true);
@@ -703,17 +714,6 @@ describe('DOMEventResponderSystem', () => {
714 );
715 ReactDOM.render(<Test />, container);
716
706 - const createEvent = (type, data) => {
707 - const event = document.createEvent('CustomEvent');
708 - event.initCustomEvent(type, true, true);
709 - if (data != null) {
710 - Object.entries(data).forEach(([key, value]) => {
711 - event[key] = value;
712 - });
713 - }
714 - return event;
715 - };
716 -
717 buttonRef.current.dispatchEvent(
718 createEvent('pointerout', {relatedTarget: divRef.current}),
719 );
@@ -1052,4 +1052,72 @@ describe('DOMEventResponderSystem', () => {
1052
1053 ReactDOM.render(<Test2 />, container);
1054 });
1055 +
1056 + it('should work with event component hooks', () => {
1057 + const buttonRef = React.createRef();
1058 + const eventLogs = [];
1059 + const EventComponent = createReactEventComponent({
1060 + targetEventTypes: ['foo'],
1061 + onEvent: (event, context, props) => {
1062 + if (props.onFoo) {
1063 + const fooEvent = {
1064 + target: event.target,
1065 + type: 'foo',
1066 + timeStamp: context.getTimeStamp(),
1067 + };
1068 + context.dispatchEvent(fooEvent, props.onFoo, DiscreteEvent);
1069 + }
1070 + },
1071 + });
1072 +
1073 + const Test = () => {
1074 + React.unstable_useEvent(EventComponent.responder, {
1075 + onFoo: e => eventLogs.push('hook'),
1076 + });
1077 + return (
1078 + <EventComponent onFoo={e => eventLogs.push('prop')}>
1079 + <button ref={buttonRef} />
1080 + </EventComponent>
1081 + );
1082 + };
1083 +
1084 + ReactDOM.render(<Test />, container);
1085 + buttonRef.current.dispatchEvent(createEvent('foo'));
1086 + expect(eventLogs).toEqual(['prop', 'hook']);
1087 +
1088 + // Clear events
1089 + eventLogs.length = 0;
1090 +
1091 + const Test2 = () => {
1092 + React.unstable_useEvent(EventComponent.responder, {
1093 + onFoo: e => eventLogs.push('hook'),
1094 + });
1095 + return <button ref={buttonRef} />;
1096 + };
1097 +
1098 + ReactDOM.render(<Test2 />, container);
1099 + buttonRef.current.dispatchEvent(createEvent('foo'));
1100 + // No events shold fire, as there are no event components in the branch
1101 + expect(eventLogs).toEqual([]);
1102 +
1103 + const Test3 = () => {
1104 + React.unstable_useEvent(EventComponent.responder, {
1105 + onFoo: e => eventLogs.push('hook 2a'),
1106 + });
1107 + React.unstable_useEvent(EventComponent.responder, {
1108 + onFoo: e => eventLogs.push('hook 2b'),
1109 + });
1110 + return (
1111 + <EventComponent onFoo={e => eventLogs.push('should not fire')}>
1112 + <EventComponent onFoo={e => eventLogs.push('prop 2')}>
1113 + <button ref={buttonRef} />
1114 + </EventComponent>
1115 + </EventComponent>
1116 + );
1117 + };
1118 +
1119 + ReactDOM.render(<Test3 />, container);
1120 + buttonRef.current.dispatchEvent(createEvent('foo'));
1121 + expect(eventLogs).toEqual(['prop 2', 'hook 2a', 'hook 2b']);
1122 + });
1123 });
packages/react-dom/src/server/ReactPartialRendererHooks.js
+1
@@ -470,4 +470,5 @@ export const Dispatcher: DispatcherType = {
470 useEffect: noop,
471 // Debugging effect
472 useDebugValue: noop,
473 + useEvent: noop,
474 };
packages/react-events/src/Drag.js
+3 -1
@@ -85,6 +85,7 @@ function dispatchDragEvent(
85 }
86
87 const DragResponder = {
88 + displayName: 'Drag',
89 targetEventTypes,
90 createInitialState(): DragState {
91 return {
@@ -98,6 +99,7 @@ const DragResponder = {
99 };
100 },
101 allowMultipleHostChildren: false,
102 + allowEventHooks: false,
103 onEvent(
104 event: ReactDOMResponderEvent,
105 context: ReactDOMResponderContext,
@@ -259,4 +261,4 @@ const DragResponder = {
261 },
262 };
263
262 -export default ReactDOM.unstable_createEvent(DragResponder, 'Drag');
264 +export default ReactDOM.unstable_createEvent(DragResponder);
packages/react-events/src/Focus.js
+3 -1
@@ -217,6 +217,7 @@ function handleRootPointerEvent(
217 let isGlobalFocusVisible = true;
218
219 const FocusResponder = {
220 + displayName: 'Focus',
221 targetEventTypes,
222 rootEventTypes,
223 createInitialState(): FocusState {
@@ -228,6 +229,7 @@ const FocusResponder = {
229 };
230 },
231 allowMultipleHostChildren: false,
232 + allowEventHooks: true,
233 onEvent(
234 event: ReactDOMResponderEvent,
235 context: ReactDOMResponderContext,
@@ -336,4 +338,4 @@ const FocusResponder = {
338 },
339 };
340
339 -export default ReactDOM.unstable_createEvent(FocusResponder, 'Focus');
341 +export default ReactDOM.unstable_createEvent(FocusResponder);
packages/react-events/src/FocusScope.js
+3 -1
@@ -46,6 +46,7 @@ function getFirstFocusableElement(
46 }
47
48 const FocusScopeResponder = {
49 + displayName: 'FocusScope',
50 targetEventTypes,
51 rootEventTypes,
52 createInitialState(): FocusScopeState {
@@ -55,6 +56,7 @@ const FocusScopeResponder = {
56 };
57 },
58 allowMultipleHostChildren: true,
59 + allowEventHooks: false,
60 onEvent(
61 event: ReactDOMResponderEvent,
62 context: ReactDOMResponderContext,
@@ -157,4 +159,4 @@ const FocusScopeResponder = {
159 },
160 };
161
160 -export default ReactDOM.unstable_createEvent(FocusScopeResponder, 'FocusScope');
162 +export default ReactDOM.unstable_createEvent(FocusScopeResponder);
packages/react-events/src/Hover.js
+3 -1
@@ -274,6 +274,7 @@ function isEmulatedMouseEvent(event, state) {
274 }
275
276 const HoverResponder = {
277 + displayName: 'Hover',
278 targetEventTypes,
279 createInitialState() {
280 return {
@@ -287,6 +288,7 @@ const HoverResponder = {
288 };
289 },
290 allowMultipleHostChildren: false,
291 + allowEventHooks: true,
292 onEvent(
293 event: ReactDOMResponderEvent,
294 context: ReactDOMResponderContext,
@@ -406,4 +408,4 @@ const HoverResponder = {
408 },
409 };
410
409 -export default ReactDOM.unstable_createEvent(HoverResponder, 'Hover');
411 +export default ReactDOM.unstable_createEvent(HoverResponder);
packages/react-events/src/Press.js
+3 -1
@@ -623,6 +623,7 @@ function updateIsPressWithinResponderRegion(
623 }
624
625 const PressResponder = {
626 + displayName: 'Press',
627 targetEventTypes,
628 createInitialState(): PressState {
629 return {
@@ -647,6 +648,7 @@ const PressResponder = {
648 };
649 },
650 allowMultipleHostChildren: false,
651 + allowEventHooks: true,
652 onEvent(
653 event: ReactDOMResponderEvent,
654 context: ReactDOMResponderContext,
@@ -979,4 +981,4 @@ const PressResponder = {
981 },
982 };
983
982 -export default ReactDOM.unstable_createEvent(PressResponder, 'Press');
984 +export default ReactDOM.unstable_createEvent(PressResponder);
packages/react-events/src/Scroll.js
+3 -1
@@ -117,6 +117,7 @@ function dispatchEvent(
117 }
118
119 const ScrollResponder = {
120 + displayName: 'Scroll',
121 targetEventTypes,
122 createInitialState() {
123 return {
@@ -126,6 +127,7 @@ const ScrollResponder = {
127 };
128 },
129 allowMultipleHostChildren: true,
130 + allowEventHooks: true,
131 onEvent(
132 event: ReactDOMResponderEvent,
133 context: ReactDOMResponderContext,
@@ -211,4 +213,4 @@ const ScrollResponder = {
213 },
214 };
215
214 -export default ReactDOM.unstable_createEvent(ScrollResponder, 'Scroll');
216 +export default ReactDOM.unstable_createEvent(ScrollResponder);
packages/react-events/src/Swipe.js
+3 -1
@@ -89,6 +89,7 @@ type SwipeState = {
89 };
90
91 const SwipeResponder = {
92 + displayName: 'Scroll',
93 targetEventTypes,
94 createInitialState(): SwipeState {
95 return {
@@ -104,6 +105,7 @@ const SwipeResponder = {
105 };
106 },
107 allowMultipleHostChildren: false,
108 + allowEventHooks: false,
109 onEvent(
110 event: ReactDOMResponderEvent,
111 context: ReactDOMResponderContext,
@@ -262,4 +264,4 @@ const SwipeResponder = {
264 },
265 };
266
265 -export default ReactDOM.unstable_createEvent(SwipeResponder, 'Swipe');
267 +export default ReactDOM.unstable_createEvent(SwipeResponder);
packages/react-events/src/__tests__/Focus-test.internal.js
+1 -1
@@ -354,6 +354,6 @@ describe('Focus event responder', () => {
354 });
355
356 it('expect displayName to show up for event component', () => {
357 - expect(Focus.displayName).toBe('Focus');
357 + expect(Focus.responder.displayName).toBe('Focus');
358 });
359 });
packages/react-events/src/__tests__/Hover-test.internal.js
+1 -1
@@ -546,7 +546,7 @@ describe('Hover event responder', () => {
546 });
547
548 it('expect displayName to show up for event component', () => {
549 - expect(Hover.displayName).toBe('Hover');
549 + expect(Hover.responder.displayName).toBe('Hover');
550 });
551
552 it('should correctly pass through event properties', () => {
packages/react-events/src/__tests__/Press-test.internal.js
+1 -1
@@ -2434,7 +2434,7 @@ describe('Event responder: Press', () => {
2434 });
2435
2436 it('expect displayName to show up for event component', () => {
2437 - expect(Press.displayName).toBe('Press');
2437 + expect(Press.responder.displayName).toBe('Press');
2438 });
2439
2440 it('should not trigger an invariant in addRootEventTypes()', () => {
packages/react-reconciler/src/ReactFiber.js
+17 -6
@@ -21,8 +21,9 @@ import type {TypeOfMode} from './ReactTypeOfMode';
21 import type {SideEffectTag} from 'shared/ReactSideEffectTags';
22 import type {ExpirationTime} from './ReactFiberExpirationTime';
23 import type {UpdateQueue} from './ReactUpdateQueue';
24 -import type {ContextDependencyList} from './ReactFiberNewContext';
24 +import type {ContextDependency} from './ReactFiberNewContext';
25 import type {HookType} from './ReactFiberHooks';
26 +import type {ReactEventComponentInstance} from 'shared/ReactTypes';
27
28 import invariant from 'shared/invariant';
29 import warningWithoutStack from 'shared/warningWithoutStack';
@@ -102,6 +103,12 @@ if (__DEV__) {
103 }
104 }
105
106 +export type Dependencies = {
107 + expirationTime: ExpirationTime,
108 + firstContext: ContextDependency<mixed> | null,
109 + events: Array<ReactEventComponentInstance> | null,
110 +};
111 +
112 // A Fiber is work on a Component that needs to be done or was done. There can
113 // be more than one per component.
114 export type Fiber = {|
@@ -162,8 +169,8 @@ export type Fiber = {|
169 // The state used to create the output
170 memoizedState: any,
171
165 - // A linked-list of contexts that this fiber depends on
166 - contextDependencies: ContextDependencyList | null,
172 + // An object of dependencies for this fiber
173 + dependencies: Dependencies,
174
175 // Bitfield that describes properties about the fiber and its subtree. E.g.
176 // the ConcurrentMode flag indicates whether the subtree should be async-by-
@@ -263,7 +270,11 @@ function FiberNode(
270 this.memoizedProps = null;
271 this.updateQueue = null;
272 this.memoizedState = null;
266 - this.contextDependencies = null;
273 + this.dependencies = {
274 + expirationTime: 0,
275 + firstContext: null,
276 + events: null,
277 + };
278
279 this.mode = mode;
280
@@ -432,7 +443,7 @@ export function createWorkInProgress(
443 workInProgress.memoizedProps = current.memoizedProps;
444 workInProgress.memoizedState = current.memoizedState;
445 workInProgress.updateQueue = current.updateQueue;
435 - workInProgress.contextDependencies = current.contextDependencies;
446 + workInProgress.dependencies = current.dependencies;
447
448 // These will be overridden during the parent's reconciliation
449 workInProgress.sibling = current.sibling;
@@ -823,7 +834,7 @@ export function assignFiberPropertiesInDEV(
834 target.memoizedProps = source.memoizedProps;
835 target.updateQueue = source.updateQueue;
836 target.memoizedState = source.memoizedState;
826 - target.contextDependencies = source.contextDependencies;
837 + target.dependencies = source.dependencies;
838 target.mode = source.mode;
839 target.effectTag = source.effectTag;
840 target.nextEffect = source.nextEffect;
packages/react-reconciler/src/ReactFiberBeginWork.js
+6 -3
@@ -177,6 +177,7 @@ import {
177 requestCurrentTime,
178 retryTimedOutBoundary,
179 } from './ReactFiberWorkLoop';
180 +import {prepareToReadEventComponents} from './ReactFiberEvents';
181
182 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
183
@@ -301,6 +302,7 @@ function updateForwardRef(
302 // The rest is a fork of updateFunctionComponent
303 let nextChildren;
304 prepareToReadContext(workInProgress, renderExpirationTime);
305 + prepareToReadEventComponents(workInProgress);
306 if (__DEV__) {
307 ReactCurrentOwner.current = workInProgress;
308 setCurrentPhase('render');
@@ -621,6 +623,7 @@ function updateFunctionComponent(
623
624 let nextChildren;
625 prepareToReadContext(workInProgress, renderExpirationTime);
626 + prepareToReadEventComponents(workInProgress);
627 if (__DEV__) {
628 ReactCurrentOwner.current = workInProgress;
629 setCurrentPhase('render');
@@ -1235,7 +1238,7 @@ function mountIndeterminateComponent(
1238 const context = getMaskedContext(workInProgress, unmaskedContext);
1239
1240 prepareToReadContext(workInProgress, renderExpirationTime);
1238 -
1241 + prepareToReadEventComponents(workInProgress);
1242 let value;
1243
1244 if (__DEV__) {
@@ -2450,8 +2453,8 @@ function bailoutOnAlreadyFinishedWork(
2453 cancelWorkTimer(workInProgress);
2454
2455 if (current !== null) {
2453 - // Reuse previous context list
2454 - workInProgress.contextDependencies = current.contextDependencies;
2456 + // Reuse previous dependencies
2457 + workInProgress.dependencies = current.dependencies;
2458 }
2459
2460 if (enableProfilerTimer) {
packages/react-reconciler/src/ReactFiberCompleteWork.js
+11 -10
@@ -115,7 +115,10 @@ import {
115 renderDidSuspend,
116 renderDidSuspendDelayIfPossible,
117 } from './ReactFiberWorkLoop';
118 -import {getEventComponentHostChildrenCount} from './ReactFiberEvents';
118 +import {
119 + getEventComponentHostChildrenCount,
120 + createEventComponentInstance,
121 +} from './ReactFiberEvents';
122 import getComponentName from 'shared/getComponentName';
123 import warning from 'shared/warning';
124
@@ -1028,20 +1031,18 @@ function completeWork(
1031 if (responder.createInitialState !== undefined) {
1032 responderState = responder.createInitialState(newProps);
1033 }
1031 - eventComponentInstance = workInProgress.stateNode = {
1032 - currentFiber: workInProgress,
1033 - props: newProps,
1034 + eventComponentInstance = workInProgress.stateNode = createEventComponentInstance(
1035 + workInProgress,
1036 + newProps,
1037 responder,
1035 - rootEventTypes: null,
1036 - rootInstance: rootContainerInstance,
1037 - state: responderState,
1038 - };
1038 + rootContainerInstance,
1039 + responderState,
1040 + true,
1041 + );
1042 markUpdate(workInProgress);
1043 } else {
1044 // Update the props on the event component state node
1045 eventComponentInstance.props = newProps;
1043 - // Update the root container, so we can properly unmount events at some point
1044 - eventComponentInstance.rootInstance = rootContainerInstance;
1046 // Update the current fiber
1047 eventComponentInstance.currentFiber = workInProgress;
1048 updateEventComponent(eventComponentInstance);
packages/react-reconciler/src/ReactFiberEvents.js
+67
@@ -8,6 +8,8 @@
8 */
9
10 import type {Fiber} from './ReactFiber';
11 +import type {ReactEventComponentInstance} from 'shared/ReactTypes';
12 +import type {EventResponder} from 'react-reconciler/src/ReactFiberHostConfig';
13
14 import {
15 HostComponent,
@@ -16,6 +18,71 @@ import {
18 SuspenseComponent,
19 Fragment,
20 } from 'shared/ReactWorkTags';
21 +import invariant from 'shared/invariant';
22 +
23 +let currentlyRenderingFiber: null | Fiber = null;
24 +let currentEventComponentInstanceIndex: number = 0;
25 +
26 +export function prepareToReadEventComponents(workInProgress: Fiber): void {
27 + currentlyRenderingFiber = workInProgress;
28 + currentEventComponentInstanceIndex = 0;
29 +}
30 +
31 +export function updateEventComponentInstance(
32 + responder: EventResponder,
33 + props: null | Object,
34 +): void {
35 + invariant(
36 + responder.allowEventHooks,
37 + 'The "%s" event responder cannot be used via the "useEvent" hook.',
38 + responder.displayName,
39 + );
40 + const dependencies = ((currentlyRenderingFiber: any): Fiber).dependencies;
41 + let events = dependencies.events;
42 + if (events === null) {
43 + dependencies.events = events = [];
44 + }
45 + if (currentEventComponentInstanceIndex === events.length) {
46 + let responderState = null;
47 + if (responder.createInitialState !== undefined) {
48 + responderState = responder.createInitialState(props);
49 + }
50 + const eventComponentInstance = createEventComponentInstance(
51 + ((currentlyRenderingFiber: any): Fiber),
52 + props,
53 + responder,
54 + null,
55 + responderState,
56 + false,
57 + );
58 + events.push(eventComponentInstance);
59 + currentEventComponentInstanceIndex++;
60 + } else {
61 + const eventComponentInstance = events[currentEventComponentInstanceIndex++];
62 + eventComponentInstance.responder = responder;
63 + eventComponentInstance.props = props;
64 + eventComponentInstance.currentFiber = ((currentlyRenderingFiber: any): Fiber);
65 + }
66 +}
67 +
68 +export function createEventComponentInstance(
69 + currentFiber: Fiber,
70 + props: null | Object,
71 + responder: EventResponder,
72 + rootInstance: mixed,
73 + state: null | Object,
74 + localPropagation: boolean,
75 +): ReactEventComponentInstance {
76 + return {
77 + currentFiber,
78 + localPropagation,
79 + props,
80 + responder,
81 + rootEventTypes: null,
82 + rootInstance,
83 + state,
84 + };
85 +}
86
87 export function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
88 return fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
packages/react-reconciler/src/ReactFiberHooks.js
+35 -1
@@ -13,11 +13,13 @@ import type {Fiber} from './ReactFiber';
13 import type {ExpirationTime} from './ReactFiberExpirationTime';
14 import type {HookEffectTag} from './ReactHookEffectTags';
15 import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
16 +import type {EventResponder} from 'react-reconciler/src/ReactFiberHostConfig';
17
18 import ReactSharedInternals from 'shared/ReactSharedInternals';
19
20 import {NoWork} from './ReactFiberExpirationTime';
21 import {readContext} from './ReactFiberNewContext';
22 +import {updateEventComponentInstance} from './ReactFiberEvents';
23 import {
24 Update as UpdateEffect,
25 Passive as PassiveEffect,
@@ -81,6 +83,7 @@ export type Dispatcher = {
83 deps: Array<mixed> | void | null,
84 ): void,
85 useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void,
86 + useEvent(responder: EventResponder, props: null | Object): void,
87 };
88
89 type Update<S, A> = {
@@ -109,7 +112,8 @@ export type HookType =
112 | 'useCallback'
113 | 'useMemo'
114 | 'useImperativeHandle'
112 - | 'useDebugValue';
115 + | 'useDebugValue'
116 + | 'useEvent';
117
118 let didWarnAboutMismatchedHooksForComponent;
119 if (__DEV__) {
@@ -1231,6 +1235,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
1235 useRef: throwInvalidHookError,
1236 useState: throwInvalidHookError,
1237 useDebugValue: throwInvalidHookError,
1238 + useEvent: updateEventComponentInstance,
1239 };
1240
1241 const HooksDispatcherOnMount: Dispatcher = {
@@ -1246,6 +1251,7 @@ const HooksDispatcherOnMount: Dispatcher = {
1251 useRef: mountRef,
1252 useState: mountState,
1253 useDebugValue: mountDebugValue,
1254 + useEvent: updateEventComponentInstance,
1255 };
1256
1257 const HooksDispatcherOnUpdate: Dispatcher = {
@@ -1261,6 +1267,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
1267 useRef: updateRef,
1268 useState: updateState,
1269 useDebugValue: updateDebugValue,
1270 + useEvent: updateEventComponentInstance,
1271 };
1272
1273 let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
@@ -1390,6 +1397,11 @@ if (__DEV__) {
1397 mountHookTypesDev();
1398 return mountDebugValue(value, formatterFn);
1399 },
1400 + useEvent(responder, props) {
1401 + currentHookNameInDev = 'useEvent';
1402 + mountHookTypesDev();
1403 + updateEventComponentInstance(responder, props);
1404 + },
1405 };
1406
1407 HooksDispatcherOnMountWithHookTypesInDEV = {
@@ -1487,6 +1499,11 @@ if (__DEV__) {
1499 updateHookTypesDev();
1500 return mountDebugValue(value, formatterFn);
1501 },
1502 + useEvent(responder, props) {
1503 + currentHookNameInDev = 'useEvent';
1504 + updateHookTypesDev();
1505 + updateEventComponentInstance(responder, props);
1506 + },
1507 };
1508
1509 HooksDispatcherOnUpdateInDEV = {
@@ -1584,6 +1601,11 @@ if (__DEV__) {
1601 updateHookTypesDev();
1602 return updateDebugValue(value, formatterFn);
1603 },
1604 + useEvent(responder, props) {
1605 + currentHookNameInDev = 'useEvent';
1606 + updateHookTypesDev();
1607 + updateEventComponentInstance(responder, props);
1608 + },
1609 };
1610
1611 InvalidNestedHooksDispatcherOnMountInDEV = {
@@ -1692,6 +1714,12 @@ if (__DEV__) {
1714 mountHookTypesDev();
1715 return mountDebugValue(value, formatterFn);
1716 },
1717 + useEvent(responder, props) {
1718 + currentHookNameInDev = 'useEvent';
1719 + warnInvalidHookAccess();
1720 + mountHookTypesDev();
1721 + updateEventComponentInstance(responder, props);
1722 + },
1723 };
1724
1725 InvalidNestedHooksDispatcherOnUpdateInDEV = {
@@ -1800,5 +1828,11 @@ if (__DEV__) {
1828 updateHookTypesDev();
1829 return updateDebugValue(value, formatterFn);
1830 },
1831 + useEvent(responder, props) {
1832 + currentHookNameInDev = 'useEvent';
1833 + warnInvalidHookAccess();
1834 + updateHookTypesDev();
1835 + updateEventComponentInstance(responder, props);
1836 + },
1837 };
1838 }
packages/react-reconciler/src/ReactFiberNewContext.js
+14 -18
@@ -12,12 +12,7 @@ import type {Fiber} from './ReactFiber';
12 import type {StackCursor} from './ReactFiberStack';
13 import type {ExpirationTime} from './ReactFiberExpirationTime';
14
15 -export type ContextDependencyList = {
16 - first: ContextDependency<mixed>,
17 - expirationTime: ExpirationTime,
18 -};
19 -
20 -type ContextDependency<T> = {
15 +export type ContextDependency<T> = {
16 context: ReactContext<T>,
17 observedBits: number,
18 next: ContextDependency<mixed> | null,
@@ -201,11 +196,12 @@ export function propagateContextChange(
196 let nextFiber;
197
198 // Visit this fiber.
204 - const list = fiber.contextDependencies;
199 + const dependencies = fiber.dependencies;
200 + const list = dependencies.firstContext;
201 if (list !== null) {
202 nextFiber = fiber.child;
203
208 - let dependency = list.first;
204 + let dependency = list;
205 while (dependency !== null) {
206 // Check if the context matches.
207 if (
@@ -239,8 +235,8 @@ export function propagateContextChange(
235 scheduleWorkOnParentPath(fiber.return, renderExpirationTime);
236
237 // Mark the expiration time on the list, too.
242 - if (list.expirationTime < renderExpirationTime) {
243 - list.expirationTime = renderExpirationTime;
238 + if (dependencies.expirationTime < renderExpirationTime) {
239 + dependencies.expirationTime = renderExpirationTime;
240 }
241
242 // Since we already found a match, we can stop traversing the
@@ -315,17 +311,18 @@ export function prepareToReadContext(
311 lastContextDependency = null;
312 lastContextWithAllBitsObserved = null;
313
318 - const currentDependencies = workInProgress.contextDependencies;
314 + const dependencies = workInProgress.dependencies;
315 + const firstContext = dependencies.firstContext;
316 if (
320 - currentDependencies !== null &&
321 - currentDependencies.expirationTime >= renderExpirationTime
317 + firstContext !== null &&
318 + dependencies.expirationTime >= renderExpirationTime
319 ) {
320 // Context list has a pending update. Mark that this fiber performed work.
321 markWorkInProgressReceivedUpdate();
322 }
323
324 // Reset the work-in-progress list
328 - workInProgress.contextDependencies = null;
325 + dependencies.firstContext = null;
326 }
327
328 export function readContext<T>(
@@ -378,10 +375,9 @@ export function readContext<T>(
375
376 // This is the first dependency for this component. Create a new list.
377 lastContextDependency = contextItem;
381 - currentlyRenderingFiber.contextDependencies = {
382 - first: contextItem,
383 - expirationTime: NoWork,
384 - };
378 + const dependencies = currentlyRenderingFiber.dependencies;
379 + dependencies.expirationTime = NoWork;
380 + dependencies.firstContext = contextItem;
381 } else {
382 // Append a new context item.
383 lastContextDependency = lastContextDependency.next = contextItem;
packages/react-reconciler/src/__tests__/ReactFiberEvents-test-internal.js
+1 -1
@@ -22,6 +22,7 @@ let EventTarget;
22 let ReactSymbols;
23
24 const noOpResponder = {
25 + displayName: 'TestEventComponent',
26 targetEventTypes: [],
27 handleEvent() {},
28 };
@@ -29,7 +30,6 @@ const noOpResponder = {
30 function createReactEventComponent() {
31 return {
32 $$typeof: ReactSymbols.REACT_EVENT_COMPONENT_TYPE,
32 - displayName: 'TestEventComponent',
33 props: null,
34 responder: noOpResponder,
35 };
packages/react-test-renderer/src/ReactShallowRenderer.js
+1
@@ -381,6 +381,7 @@ class ReactShallowRenderer {
381 useReducer,
382 useRef,
383 useState,
384 + useEvent: noOp,
385 };
386 }
387
packages/react/src/React.js
+6 -1
@@ -39,6 +39,7 @@ import {
39 useReducer,
40 useRef,
41 useState,
42 + useEvent,
43 } from './ReactHooks';
44 import {withSuspenseConfig} from './ReactBatchConfig';
45 import {
@@ -51,7 +52,7 @@ import {
52 } from './ReactElementValidator';
53 import ReactSharedInternals from './ReactSharedInternals';
54 import {error, warn} from './withComponentStack';
54 -import {enableJSXTransformAPI} from 'shared/ReactFeatureFlags';
55 +import {enableJSXTransformAPI, enableEventAPI} from 'shared/ReactFeatureFlags';
56 const React = {
57 Children: {
58 map,
@@ -120,4 +121,8 @@ if (enableJSXTransformAPI) {
121 }
122 }
123
124 +if (enableEventAPI) {
125 + React.unstable_useEvent = useEvent;
126 +}
127 +
128 export default React;
packages/react/src/ReactHooks.js
+6
@@ -8,6 +8,7 @@
8 */
9
10 import type {ReactContext} from 'shared/ReactTypes';
11 +import type {EventResponder} from 'react-reconciler/src/ReactFiberHostConfig';
12 import invariant from 'shared/invariant';
13 import warning from 'shared/warning';
14
@@ -135,3 +136,8 @@ export function useDebugValue(value: any, formatterFn: ?(value: any) => any) {
136 return dispatcher.useDebugValue(value, formatterFn);
137 }
138 }
139 +
140 +export function useEvent(responder: EventResponder, props: null | Object) {
141 + const dispatcher = resolveDispatcher();
142 + return dispatcher.useEvent(responder, props);
143 +}
packages/shared/ReactDOMTypes.js
+2 -1
@@ -34,11 +34,12 @@ export type ReactDOMResponderEvent = {
34 };
35
36 export type ReactDOMEventResponder = {
37 + displayName: string,
38 targetEventTypes?: Array<ReactDOMEventResponderEventType>,
39 rootEventTypes?: Array<ReactDOMEventResponderEventType>,
40 createInitialState?: (props: null | Object) => Object,
41 allowMultipleHostChildren: boolean,
41 - stopLocalPropagation: boolean,
42 + allowEventHooks: boolean,
43 onEvent?: (
44 event: ReactDOMResponderEvent,
45 context: ReactDOMResponderContext,
packages/shared/ReactTypes.js
+2 -2
@@ -86,16 +86,16 @@ export type RefObject = {|
86
87 export type ReactEventComponentInstance = {|
88 currentFiber: mixed,
89 + localPropagation: boolean,
90 props: null | Object,
91 responder: EventResponder,
92 rootEventTypes: null | Set<string>,
92 - rootInstance: mixed,
93 + rootInstance: null | mixed,
94 state: null | Object,
95 |};
96
97 export type ReactEventComponent = {|
98 $$typeof: Symbol | number,
98 - displayName: string,
99 props: null | Object,
100 responder: EventResponder,
101 |};
packages/shared/createEventComponent.js
-2
@@ -31,7 +31,6 @@ if (__DEV__) {
31
32 export default function createEventComponent(
33 responder: EventResponder,
34 - displayName: string,
34 ): ReactEventComponent {
35 // We use responder as a Map key later on. When we have a bad
36 // polyfill, then we can't use it as a key as the polyfill tries
@@ -41,7 +40,6 @@ export default function createEventComponent(
40 }
41 const eventComponent = {
42 $$typeof: REACT_EVENT_COMPONENT_TYPE,
44 - displayName: displayName,
43 props: null,
44 responder: responder,
45 };
packages/shared/getComponentName.js
+1 -1
@@ -98,7 +98,7 @@ function getComponentName(type: mixed): string | null {
98 case REACT_EVENT_COMPONENT_TYPE: {
99 if (enableEventAPI) {
100 const eventComponent = ((type: any): ReactEventComponent);
101 - return eventComponent.displayName;
101 + return eventComponent.responder.displayName;
102 }
103 break;
104 }
scripts/error-codes/codes.json
+2 -1
@@ -333,5 +333,6 @@
333 "332": "Unknown priority level.",
334 "333": "This should have a parent host component initialized. This error is likely caused by a bug in React. Please file an issue.",
335 "334": "accumulate(...): Accumulated items must not be null or undefined.",
336 - "335": "ReactDOMServer does not yet support the event API."
336 + "335": "ReactDOMServer does not yet support the event API.",
337 + "336": "The \"%s\" event responder cannot be used via the \"useEvent\" hook."
338 }