ReactDOM.useEvent: Add more scaffolding for useEvent hook (#18271)
Dominic Gannaway committed
Mar 10, 2020 at 20:31 UTC
160505b0ca143fa458926b623a05cf2645ba799a
10 files changed
+215
-1
packages/react-debug-tools/src/ReactDebugHooks.js
+16
@@ -39,6 +39,11 @@ type HookLogEntry = {
39
...
40
};
41
42
+type ReactDebugListenerMap = {|
43
+ clear: () => void,
44
+ setListener: (instance: EventTarget, callback: ?(Event) => void) => void,
45
+|};
46
+
47
let hookLog: Array<HookLogEntry> = [];
48
49
// Primitives
@@ -256,6 +261,16 @@ function useTransition(
261
return [callback => {}, false];
262
}
263
264
+const noOp = () => {};
265
+
266
+function useEvent(event: any): ReactDebugListenerMap {
267
+ hookLog.push({primitive: 'Event', stackError: new Error(), value: event});
268
+ return {
269
+ clear: noOp,
270
+ setListener: noOp,
271
+ };
272
+}
273
+
274
function useDeferredValue<T>(value: T, config: TimeoutConfig | null | void): T {
275
// useDeferredValue() composes multiple hooks internally.
276
// Advance the current hook index the same number of times
@@ -285,6 +300,7 @@ const Dispatcher: DispatcherType = {
300
useResponder,
301
useTransition,
302
useDeferredValue,
303
+ useEvent,
304
};
305
306
// Inspect
packages/react-dom/src/client/ReactDOMHostConfig.js
+7
@@ -51,6 +51,9 @@ import type {
51
ReactDOMEventResponder,
52
ReactDOMEventResponderInstance,
53
ReactDOMFundamentalComponentInstance,
54
+ ReactDOMListener,
55
+ ReactDOMListenerEvent,
56
+ ReactDOMListenerMap,
57
} from 'shared/ReactDOMTypes';
58
import {
59
mountEventResponder,
@@ -70,6 +73,10 @@ import {
73
IS_PASSIVE,
74
} from 'legacy-events/EventSystemFlags';
75
76
+export type ReactListenerEvent = ReactDOMListenerEvent;
77
+export type ReactListenerMap = ReactDOMListenerMap;
78
+export type ReactListener = ReactDOMListener;
79
+
80
export type Type = string;
81
export type Props = {
82
autoFocus?: boolean,
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+9
@@ -13,6 +13,7 @@ import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
13
import type {Fiber} from 'react-reconciler/src/ReactFiber';
14
import type {PluginModule} from 'legacy-events/PluginModuleType';
15
import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
16
+import type {ReactDOMListener} from 'shared/ReactDOMTypes';
17
18
import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
19
import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching';
@@ -296,3 +297,11 @@ export function dispatchEventForPluginEventSystem(
297
),
298
);
299
}
300
+
301
+export function attachElementListener(listener: ReactDOMListener): void {
302
+ // TODO
303
+}
304
+
305
+export function detachElementListener(listener: ReactDOMListener): void {
306
+ // TODO
307
+}
packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
+36
@@ -1039,4 +1039,40 @@ describe('DOMModernPluginEventSystem', () => {
1039
expect(log).toEqual([]);
1040
expect(onDivClick).toHaveBeenCalledTimes(0);
1041
});
1042
+
1043
+ describe('ReactDOM.useEvent', () => {
1044
+ beforeEach(() => {
1045
+ jest.resetModules();
1046
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
1047
+ ReactFeatureFlags.enableModernEventSystem = true;
1048
+ ReactFeatureFlags.enableUseEventAPI = true;
1049
+
1050
+ React = require('react');
1051
+ ReactDOM = require('react-dom');
1052
+ Scheduler = require('scheduler');
1053
+ ReactDOMServer = require('react-dom/server');
1054
+ });
1055
+
1056
+ if (!__EXPERIMENTAL__) {
1057
+ it("empty test so Jest doesn't complain", () => {});
1058
+ return;
1059
+ }
1060
+
1061
+ it('should create the same event listener map', () => {
1062
+ let listenerMaps = [];
1063
+
1064
+ function Test() {
1065
+ const listenerMap = ReactDOM.unstable_useEvent('click');
1066
+
1067
+ listenerMaps.push(listenerMap);
1068
+
1069
+ return <div />;
1070
+ }
1071
+
1072
+ ReactDOM.render(<Test />, container);
1073
+ ReactDOM.render(<Test />, container);
1074
+ expect(listenerMaps.length).toEqual(2);
1075
+ expect(listenerMaps[0]).toEqual(listenerMaps[1]);
1076
+ });
1077
+ });
1078
});
packages/react-dom/src/server/ReactPartialRendererHooks.js
+10
@@ -17,6 +17,8 @@ import type {
17
ReactEventResponderListener,
18
} from 'shared/ReactTypes';
19
import type {SuspenseConfig} from 'react-reconciler/src/ReactFiberSuspenseConfig';
20
+import type {ReactDOMListenerMap} from 'shared/ReactDOMTypes';
21
+
22
import {validateContextBounds} from './ReactPartialRendererContext';
23
24
import invariant from 'shared/invariant';
@@ -474,6 +476,13 @@ function useTransition(
476
return [startTransition, false];
477
}
478
479
+function useEvent(event: any): ReactDOMListenerMap {
480
+ return {
481
+ clear: noop,
482
+ setListener: noop,
483
+ };
484
+}
485
+
486
function noop(): void {}
487
488
export let currentThreadID: ThreadID = 0;
@@ -500,4 +509,5 @@ export const Dispatcher: DispatcherType = {
509
useResponder,
510
useDeferredValue,
511
useTransition,
512
+ useEvent,
513
};
packages/react-native-renderer/src/ReactFabricHostConfig.js
+4
@@ -76,6 +76,10 @@ export type UpdatePayload = Object;
76
export type TimeoutHandle = TimeoutID;
77
export type NoTimeout = -1;
78
79
+export type ReactListenerEvent = Object;
80
+export type ReactListenerMap = Object;
81
+export type ReactListener = Object;
82
+
83
// TODO: Remove this conditional once all changes have propagated.
84
if (registerEventHandler) {
85
/**
packages/react-native-renderer/src/ReactNativeHostConfig.js
+4
@@ -26,6 +26,10 @@ import ReactNativeFiberHostComponent from './ReactNativeFiberHostComponent';
26
27
const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
28
29
+export type ReactListenerEvent = Object;
30
+export type ReactListenerMap = Object;
31
+export type ReactListener = Object;
32
+
33
export type Type = string;
34
export type Props = Object;
35
export type Container = number;
packages/react-reconciler/src/ReactFiberHooks.js
+122
-1
@@ -17,8 +17,13 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
17
import type {HookEffectTag} from './ReactHookEffectTags';
18
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
19
import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
20
+import type {
21
+ ReactListenerEvent,
22
+ ReactListenerMap,
23
+} from './ReactFiberHostConfig';
24
25
import ReactSharedInternals from 'shared/ReactSharedInternals';
26
+import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
27
28
import {NoWork, Sync} from './ReactFiberExpirationTime';
29
import {readContext} from './ReactFiberNewContext';
@@ -28,6 +33,7 @@ import {
33
Passive as PassiveEffect,
34
} from 'shared/ReactSideEffectTags';
35
import {
36
+ NoEffect as NoHookEffect,
37
HasEffect as HookHasEffect,
38
Layout as HookLayout,
39
Passive as HookPassive,
@@ -97,6 +103,7 @@ export type Dispatcher = {|
103
useTransition(
104
config: SuspenseConfig | void | null,
105
): [(() => void) => void, boolean],
106
+ useEvent(event: ReactListenerEvent): ReactListenerMap,
107
|};
108
109
type Update<S, A> = {|
@@ -129,7 +136,8 @@ export type HookType =
136
| 'useDebugValue'
137
| 'useResponder'
138
| 'useDeferredValue'
132
- | 'useTransition';
139
+ | 'useTransition'
140
+ | 'useEvent';
141
142
let didWarnAboutMismatchedHooksForComponent;
143
if (__DEV__) {
@@ -1369,6 +1377,77 @@ function dispatchAction<S, A>(
1377
}
1378
}
1379
1380
+const noOpMount = () => {};
1381
+
1382
+function mountEventListener(event: ReactListenerEvent): ReactListenerMap {
1383
+ if (enableUseEventAPI) {
1384
+ const hook = mountWorkInProgressHook();
1385
+
1386
+ const clear = () => {
1387
+ // TODO
1388
+ };
1389
+
1390
+ const reactListenerMap: ReactListenerMap = {
1391
+ clear,
1392
+ setListener(instance: EventTarget, callback: ?(Event) => void): void {
1393
+ // TODO
1394
+ },
1395
+ };
1396
+ // In order to clear up upon the hook unmounting,
1397
+ // we ensure we set the effecrt tag so that we visit
1398
+ // this effect in the commit phase, so we can handle
1399
+ // clean-up accordingly.
1400
+ currentlyRenderingFiber.effectTag |= UpdateEffect;
1401
+ pushEffect(NoHookEffect, noOpMount, clear, null);
1402
+ hook.memoizedState = [reactListenerMap, event, clear];
1403
+ return reactListenerMap;
1404
+ }
1405
+ // To make Flow not complain
1406
+ return (undefined: any);
1407
+}
1408
+
1409
+function updateEventListener(event: ReactListenerEvent): ReactListenerMap {
1410
+ if (enableUseEventAPI) {
1411
+ const hook = updateWorkInProgressHook();
1412
+ const [reactListenerMap, memoizedEvent, clear] = hook.memoizedState;
1413
+ if (__DEV__) {
1414
+ if (memoizedEvent.type !== event.type) {
1415
+ console.warn(
1416
+ 'The event type argument passed to the useEvent() hook was different between renders.' +
1417
+ ' The event type is static and should never change between renders.',
1418
+ );
1419
+ }
1420
+ if (memoizedEvent.capture !== event.capture) {
1421
+ console.warn(
1422
+ 'The "capture" option passed to the useEvent() hook was different between renders.' +
1423
+ ' The "capture" option is static and should never change between renders.',
1424
+ );
1425
+ }
1426
+ if (memoizedEvent.priority !== event.priority) {
1427
+ console.warn(
1428
+ 'The "priority" option passed to the useEvent() hook was different between renders.' +
1429
+ ' The "priority" option is static and should never change between renders.',
1430
+ );
1431
+ }
1432
+ if (memoizedEvent.passive !== event.passive) {
1433
+ console.warn(
1434
+ 'The "passive" option passed to the useEvent() hook was different between renders.' +
1435
+ ' The "passive" option is static and should never change between renders.',
1436
+ );
1437
+ }
1438
+ }
1439
+ // In order to clear up upon the hook unmounting,
1440
+ // we ensure we set the effecrt tag so that we visit
1441
+ // this effect in the commit phase, so we can handle
1442
+ // clean-up accordingly.
1443
+ currentlyRenderingFiber.effectTag |= UpdateEffect;
1444
+ pushEffect(NoHookEffect, noOpMount, clear, null);
1445
+ return reactListenerMap;
1446
+ }
1447
+ // To make Flow not complain
1448
+ return (undefined: any);
1449
+}
1450
+
1451
export const ContextOnlyDispatcher: Dispatcher = {
1452
readContext,
1453
@@ -1385,6 +1464,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
1464
useResponder: throwInvalidHookError,
1465
useDeferredValue: throwInvalidHookError,
1466
useTransition: throwInvalidHookError,
1467
+ useEvent: throwInvalidHookError,
1468
};
1469
1470
const HooksDispatcherOnMount: Dispatcher = {
@@ -1403,6 +1483,7 @@ const HooksDispatcherOnMount: Dispatcher = {
1483
useResponder: createDeprecatedResponderListener,
1484
useDeferredValue: mountDeferredValue,
1485
useTransition: mountTransition,
1486
+ useEvent: mountEventListener,
1487
};
1488
1489
const HooksDispatcherOnUpdate: Dispatcher = {
@@ -1421,6 +1502,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
1502
useResponder: createDeprecatedResponderListener,
1503
useDeferredValue: updateDeferredValue,
1504
useTransition: updateTransition,
1505
+ useEvent: updateEventListener,
1506
};
1507
1508
const HooksDispatcherOnRerender: Dispatcher = {
@@ -1439,6 +1521,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
1521
useResponder: createDeprecatedResponderListener,
1522
useDeferredValue: rerenderDeferredValue,
1523
useTransition: rerenderTransition,
1524
+ useEvent: updateEventListener,
1525
};
1526
1527
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
@@ -1588,6 +1671,11 @@ if (__DEV__) {
1671
mountHookTypesDev();
1672
return mountTransition(config);
1673
},
1674
+ useEvent(event: ReactListenerEvent): ReactListenerMap {
1675
+ currentHookNameInDev = 'useEvent';
1676
+ mountHookTypesDev();
1677
+ return mountEventListener(event);
1678
+ },
1679
};
1680
1681
HooksDispatcherOnMountWithHookTypesInDEV = {
@@ -1705,6 +1793,11 @@ if (__DEV__) {
1793
updateHookTypesDev();
1794
return mountTransition(config);
1795
},
1796
+ useEvent(event: ReactListenerEvent): ReactListenerMap {
1797
+ currentHookNameInDev = 'useEvent';
1798
+ updateHookTypesDev();
1799
+ return mountEventListener(event);
1800
+ },
1801
};
1802
1803
HooksDispatcherOnUpdateInDEV = {
@@ -1822,6 +1915,11 @@ if (__DEV__) {
1915
updateHookTypesDev();
1916
return updateTransition(config);
1917
},
1918
+ useEvent(event: ReactListenerEvent): ReactListenerMap {
1919
+ currentHookNameInDev = 'useEvent';
1920
+ updateHookTypesDev();
1921
+ return updateEventListener(event);
1922
+ },
1923
};
1924
1925
HooksDispatcherOnRerenderInDEV = {
@@ -1939,6 +2037,11 @@ if (__DEV__) {
2037
updateHookTypesDev();
2038
return rerenderTransition(config);
2039
},
2040
+ useEvent(event: ReactListenerEvent): ReactListenerMap {
2041
+ currentHookNameInDev = 'useEvent';
2042
+ updateHookTypesDev();
2043
+ return updateEventListener(event);
2044
+ },
2045
};
2046
2047
InvalidNestedHooksDispatcherOnMountInDEV = {
@@ -2070,6 +2173,12 @@ if (__DEV__) {
2173
mountHookTypesDev();
2174
return mountTransition(config);
2175
},
2176
+ useEvent(event: ReactListenerEvent): ReactListenerMap {
2177
+ currentHookNameInDev = 'useEvent';
2178
+ warnInvalidHookAccess();
2179
+ mountHookTypesDev();
2180
+ return mountEventListener(event);
2181
+ },
2182
};
2183
2184
InvalidNestedHooksDispatcherOnUpdateInDEV = {
@@ -2201,6 +2310,12 @@ if (__DEV__) {
2310
updateHookTypesDev();
2311
return updateTransition(config);
2312
},
2313
+ useEvent(event: ReactListenerEvent): ReactListenerMap {
2314
+ currentHookNameInDev = 'useEvent';
2315
+ warnInvalidHookAccess();
2316
+ updateHookTypesDev();
2317
+ return updateEventListener(event);
2318
+ },
2319
};
2320
2321
InvalidNestedHooksDispatcherOnRerenderInDEV = {
@@ -2332,5 +2447,11 @@ if (__DEV__) {
2447
updateHookTypesDev();
2448
return rerenderTransition(config);
2449
},
2450
+ useEvent(event: ReactListenerEvent): ReactListenerMap {
2451
+ currentHookNameInDev = 'useEvent';
2452
+ warnInvalidHookAccess();
2453
+ updateHookTypesDev();
2454
+ return updateEventListener(event);
2455
+ },
2456
};
2457
}
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+3
@@ -38,6 +38,9 @@ export opaque type ChildSet = mixed; // eslint-disable-line no-undef
38
export opaque type TimeoutHandle = mixed; // eslint-disable-line no-undef
39
export opaque type NoTimeout = mixed; // eslint-disable-line no-undef
40
export type EventResponder = any;
41
+export type ReactListenerEvent = Object;
42
+export type ReactListenerMap = Object;
43
+export type ReactListener = Object;
44
45
export const getPublicInstance = $$$hostConfig.getPublicInstance;
46
export const getRootHostContext = $$$hostConfig.getRootHostContext;
packages/react-test-renderer/src/ReactTestHostConfig.js
+4
@@ -45,6 +45,10 @@ export type TimeoutHandle = TimeoutID;
45
export type NoTimeout = -1;
46
export type EventResponder = any;
47
48
+export type ReactListenerEvent = Object;
49
+export type ReactListenerMap = Object;
50
+export type ReactListener = Object;
51
+
52
export * from 'shared/HostConfigWithNoPersistence';
53
export * from 'shared/HostConfigWithNoHydration';
54