Skip to content

Commit 72d8aba

Browse files
committed
ReactDOM.useEvent: Add more scaffolding for useEvent hook
Add test Fix Address feedback fix Fix flow types Fix Fix
1 parent a3bf668 commit 72d8aba

11 files changed

Lines changed: 199 additions & 1 deletion

File tree

packages/react-debug-tools/src/ReactDebugHooks.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ type HookLogEntry = {
3939
...
4040
};
4141

42+
type ReactDebugListenerMap = {|
43+
clear: () => void,
44+
setListener: (instance: EventTarget, callback: ?(Event) => void) => void,
45+
|};
46+
4247
let hookLog: Array<HookLogEntry> = [];
4348

4449
// Primitives
@@ -256,6 +261,16 @@ function useTransition(
256261
return [callback => {}, false];
257262
}
258263

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+
259274
function useDeferredValue<T>(value: T, config: TimeoutConfig | null | void): T {
260275
// useDeferredValue() composes multiple hooks internally.
261276
// Advance the current hook index the same number of times
@@ -285,6 +300,7 @@ const Dispatcher: DispatcherType = {
285300
useResponder,
286301
useTransition,
287302
useDeferredValue,
303+
useEvent,
288304
};
289305

290306
// Inspect

packages/react-dom/src/client/ReactDOMHostConfig.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ import type {
5151
ReactDOMEventResponder,
5252
ReactDOMEventResponderInstance,
5353
ReactDOMFundamentalComponentInstance,
54+
ReactDOMListener,
55+
ReactDOMListenerEvent,
56+
ReactDOMListenerMap,
5457
} from 'shared/ReactDOMTypes';
5558
import {
5659
mountEventResponder,
@@ -70,6 +73,10 @@ import {
7073
IS_PASSIVE,
7174
} from 'legacy-events/EventSystemFlags';
7275

76+
export type ReactListenerEvent = ReactDOMListenerEvent;
77+
export type ReactListenerMap = ReactDOMListenerMap;
78+
export type ReactListener = ReactDOMListener;
79+
7380
export type Type = string;
7481
export type Props = {
7582
autoFocus?: boolean,

packages/react-dom/src/events/DOMModernPluginEventSystem.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
1313
import type {Fiber} from 'react-reconciler/src/ReactFiber';
1414
import type {PluginModule} from 'legacy-events/PluginModuleType';
1515
import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
16+
import type {ReactDOMListener} from 'shared/ReactDOMTypes';
1617

1718
import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
1819
import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching';
@@ -296,3 +297,11 @@ export function dispatchEventForPluginEventSystem(
296297
),
297298
);
298299
}
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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe('DOMModernPluginEventSystem', () => {
2828
jest.resetModules();
2929
ReactFeatureFlags = require('shared/ReactFeatureFlags');
3030
ReactFeatureFlags.enableModernEventSystem = true;
31+
ReactFeatureFlags.enableUseEventAPI = true;
3132

3233
React = require('react');
3334
ReactDOM = require('react-dom');
@@ -1039,4 +1040,23 @@ describe('DOMModernPluginEventSystem', () => {
10391040
expect(log).toEqual([]);
10401041
expect(onDivClick).toHaveBeenCalledTimes(0);
10411042
});
1043+
1044+
describe('ReactDOM.useEvent', () => {
1045+
it('should create the same event listener map', () => {
1046+
let listenerMaps = [];
1047+
1048+
function Test() {
1049+
const listenerMap = ReactDOM.unstable_useEvent('click');
1050+
1051+
listenerMaps.push(listenerMap);
1052+
1053+
return <div />;
1054+
}
1055+
1056+
ReactDOM.render(<Test />, container);
1057+
ReactDOM.render(<Test />, container);
1058+
expect(listenerMaps.length).toEqual(2);
1059+
expect(listenerMaps[0]).toEqual(listenerMaps[1]);
1060+
});
1061+
});
10421062
});

packages/react-dom/src/server/ReactPartialRendererHooks.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import type {
1717
ReactEventResponderListener,
1818
} from 'shared/ReactTypes';
1919
import type {SuspenseConfig} from 'react-reconciler/src/ReactFiberSuspenseConfig';
20+
import type {ReactDOMListenerMap} from 'shared/ReactDOMTypes';
21+
2022
import {validateContextBounds} from './ReactPartialRendererContext';
2123

2224
import invariant from 'shared/invariant';
@@ -474,6 +476,13 @@ function useTransition(
474476
return [startTransition, false];
475477
}
476478

479+
function useEvent(event: any): ReactDOMListenerMap {
480+
return {
481+
clear: noop,
482+
setListener: noop,
483+
};
484+
}
485+
477486
function noop(): void {}
478487

479488
export let currentThreadID: ThreadID = 0;
@@ -500,4 +509,5 @@ export const Dispatcher: DispatcherType = {
500509
useResponder,
501510
useDeferredValue,
502511
useTransition,
512+
useEvent,
503513
};

packages/react-native-renderer/src/ReactFabricHostConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ export type UpdatePayload = Object;
7676
export type TimeoutHandle = TimeoutID;
7777
export type NoTimeout = -1;
7878

79+
export type ReactListenerEvent = Object;
80+
export type ReactListenerMap = Object;
81+
export type ReactListener = Object;
82+
7983
// TODO: Remove this conditional once all changes have propagated.
8084
if (registerEventHandler) {
8185
/**

packages/react-native-renderer/src/ReactNativeHostConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import ReactNativeFiberHostComponent from './ReactNativeFiberHostComponent';
2626

2727
const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
2828

29+
export type ReactListenerEvent = Object;
30+
export type ReactListenerMap = Object;
31+
export type ReactListener = Object;
32+
2933
export type Type = string;
3034
export type Props = Object;
3135
export type Container = number;

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
1717
import type {HookEffectTag} from './ReactHookEffectTags';
1818
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
1919
import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
20+
import type {
21+
ReactListenerEvent,
22+
ReactListenerMap,
23+
} from './ReactFiberHostConfig';
2024

2125
import ReactSharedInternals from 'shared/ReactSharedInternals';
26+
import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
2227

2328
import {NoWork, Sync} from './ReactFiberExpirationTime';
2429
import {readContext} from './ReactFiberNewContext';
@@ -28,6 +33,7 @@ import {
2833
Passive as PassiveEffect,
2934
} from 'shared/ReactSideEffectTags';
3035
import {
36+
NoEffect as NoHookEffect,
3137
HasEffect as HookHasEffect,
3238
Layout as HookLayout,
3339
Passive as HookPassive,
@@ -97,6 +103,7 @@ export type Dispatcher = {|
97103
useTransition(
98104
config: SuspenseConfig | void | null,
99105
): [(() => void) => void, boolean],
106+
useEvent(event: ReactListenerEvent): ReactListenerMap,
100107
|};
101108

102109
type Update<S, A> = {|
@@ -129,7 +136,8 @@ export type HookType =
129136
| 'useDebugValue'
130137
| 'useResponder'
131138
| 'useDeferredValue'
132-
| 'useTransition';
139+
| 'useTransition'
140+
| 'useEvent';
133141

134142
let didWarnAboutMismatchedHooksForComponent;
135143
if (__DEV__) {
@@ -1369,6 +1377,77 @@ function dispatchAction<S, A>(
13691377
}
13701378
}
13711379

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+
13721451
export const ContextOnlyDispatcher: Dispatcher = {
13731452
readContext,
13741453

@@ -1385,6 +1464,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
13851464
useResponder: throwInvalidHookError,
13861465
useDeferredValue: throwInvalidHookError,
13871466
useTransition: throwInvalidHookError,
1467+
useEvent: throwInvalidHookError,
13881468
};
13891469

13901470
const HooksDispatcherOnMount: Dispatcher = {
@@ -1403,6 +1483,7 @@ const HooksDispatcherOnMount: Dispatcher = {
14031483
useResponder: createDeprecatedResponderListener,
14041484
useDeferredValue: mountDeferredValue,
14051485
useTransition: mountTransition,
1486+
useEvent: mountEventListener,
14061487
};
14071488

14081489
const HooksDispatcherOnUpdate: Dispatcher = {
@@ -1421,6 +1502,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
14211502
useResponder: createDeprecatedResponderListener,
14221503
useDeferredValue: updateDeferredValue,
14231504
useTransition: updateTransition,
1505+
useEvent: updateEventListener,
14241506
};
14251507

14261508
const HooksDispatcherOnRerender: Dispatcher = {
@@ -1439,6 +1521,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
14391521
useResponder: createDeprecatedResponderListener,
14401522
useDeferredValue: rerenderDeferredValue,
14411523
useTransition: rerenderTransition,
1524+
useEvent: updateEventListener,
14421525
};
14431526

14441527
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
@@ -1588,6 +1671,11 @@ if (__DEV__) {
15881671
mountHookTypesDev();
15891672
return mountTransition(config);
15901673
},
1674+
useEvent(event: ReactListenerEvent): ReactListenerMap {
1675+
currentHookNameInDev = 'useEvent';
1676+
mountHookTypesDev();
1677+
return mountEventListener(event);
1678+
},
15911679
};
15921680

15931681
HooksDispatcherOnMountWithHookTypesInDEV = {
@@ -1705,6 +1793,11 @@ if (__DEV__) {
17051793
updateHookTypesDev();
17061794
return mountTransition(config);
17071795
},
1796+
useEvent(event: ReactListenerEvent): ReactListenerMap {
1797+
currentHookNameInDev = 'useEvent';
1798+
updateHookTypesDev();
1799+
return mountEventListener(event);
1800+
},
17081801
};
17091802

17101803
HooksDispatcherOnUpdateInDEV = {
@@ -1822,6 +1915,11 @@ if (__DEV__) {
18221915
updateHookTypesDev();
18231916
return updateTransition(config);
18241917
},
1918+
useEvent(event: ReactListenerEvent): ReactListenerMap {
1919+
currentHookNameInDev = 'useEvent';
1920+
updateHookTypesDev();
1921+
return updateEventListener(event);
1922+
},
18251923
};
18261924

18271925
HooksDispatcherOnRerenderInDEV = {
@@ -1939,6 +2037,11 @@ if (__DEV__) {
19392037
updateHookTypesDev();
19402038
return rerenderTransition(config);
19412039
},
2040+
useEvent(event: ReactListenerEvent): ReactListenerMap {
2041+
currentHookNameInDev = 'useEvent';
2042+
updateHookTypesDev();
2043+
return updateEventListener(event);
2044+
},
19422045
};
19432046

19442047
InvalidNestedHooksDispatcherOnMountInDEV = {
@@ -2070,6 +2173,12 @@ if (__DEV__) {
20702173
mountHookTypesDev();
20712174
return mountTransition(config);
20722175
},
2176+
useEvent(event: ReactListenerEvent): ReactListenerMap {
2177+
currentHookNameInDev = 'useEvent';
2178+
warnInvalidHookAccess();
2179+
mountHookTypesDev();
2180+
return mountEventListener(event);
2181+
},
20732182
};
20742183

20752184
InvalidNestedHooksDispatcherOnUpdateInDEV = {
@@ -2201,6 +2310,12 @@ if (__DEV__) {
22012310
updateHookTypesDev();
22022311
return updateTransition(config);
22032312
},
2313+
useEvent(event: ReactListenerEvent): ReactListenerMap {
2314+
currentHookNameInDev = 'useEvent';
2315+
warnInvalidHookAccess();
2316+
updateHookTypesDev();
2317+
return updateEventListener(event);
2318+
},
22042319
};
22052320

22062321
InvalidNestedHooksDispatcherOnRerenderInDEV = {
@@ -2332,5 +2447,11 @@ if (__DEV__) {
23322447
updateHookTypesDev();
23332448
return rerenderTransition(config);
23342449
},
2450+
useEvent(event: ReactListenerEvent): ReactListenerMap {
2451+
currentHookNameInDev = 'useEvent';
2452+
warnInvalidHookAccess();
2453+
updateHookTypesDev();
2454+
return updateEventListener(event);
2455+
},
23352456
};
23362457
}

0 commit comments

Comments
 (0)