@samitouri / QOS-React / commits / 160505b0ca

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