[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
}