ReactDOM.useEvent: Add more scaffolding for useEvent hook (#18271)

This commit is contained in:
Dominic Gannaway
2020-03-10 20:31:12 +00:00
committed by GitHub
parent a3bf668812
commit 160505b0ca
10 changed files with 215 additions and 1 deletions
+16
View File
@@ -39,6 +39,11 @@ type HookLogEntry = {
...
};
type ReactDebugListenerMap = {|
clear: () => void,
setListener: (instance: EventTarget, callback: ?(Event) => void) => void,
|};
let hookLog: Array<HookLogEntry> = [];
// Primitives
@@ -256,6 +261,16 @@ function useTransition(
return [callback => {}, false];
}
const noOp = () => {};
function useEvent(event: any): ReactDebugListenerMap {
hookLog.push({primitive: 'Event', stackError: new Error(), value: event});
return {
clear: noOp,
setListener: noOp,
};
}
function useDeferredValue<T>(value: T, config: TimeoutConfig | null | void): T {
// useDeferredValue() composes multiple hooks internally.
// Advance the current hook index the same number of times
@@ -285,6 +300,7 @@ const Dispatcher: DispatcherType = {
useResponder,
useTransition,
useDeferredValue,
useEvent,
};
// Inspect
+7
View File
@@ -51,6 +51,9 @@ import type {
ReactDOMEventResponder,
ReactDOMEventResponderInstance,
ReactDOMFundamentalComponentInstance,
ReactDOMListener,
ReactDOMListenerEvent,
ReactDOMListenerMap,
} from 'shared/ReactDOMTypes';
import {
mountEventResponder,
@@ -70,6 +73,10 @@ import {
IS_PASSIVE,
} from 'legacy-events/EventSystemFlags';
export type ReactListenerEvent = ReactDOMListenerEvent;
export type ReactListenerMap = ReactDOMListenerMap;
export type ReactListener = ReactDOMListener;
export type Type = string;
export type Props = {
autoFocus?: boolean,
@@ -13,6 +13,7 @@ import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import type {PluginModule} from 'legacy-events/PluginModuleType';
import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
import type {ReactDOMListener} from 'shared/ReactDOMTypes';
import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching';
@@ -296,3 +297,11 @@ export function dispatchEventForPluginEventSystem(
),
);
}
export function attachElementListener(listener: ReactDOMListener): void {
// TODO
}
export function detachElementListener(listener: ReactDOMListener): void {
// TODO
}
@@ -1039,4 +1039,40 @@ describe('DOMModernPluginEventSystem', () => {
expect(log).toEqual([]);
expect(onDivClick).toHaveBeenCalledTimes(0);
});
describe('ReactDOM.useEvent', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableModernEventSystem = true;
ReactFeatureFlags.enableUseEventAPI = true;
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
ReactDOMServer = require('react-dom/server');
});
if (!__EXPERIMENTAL__) {
it("empty test so Jest doesn't complain", () => {});
return;
}
it('should create the same event listener map', () => {
let listenerMaps = [];
function Test() {
const listenerMap = ReactDOM.unstable_useEvent('click');
listenerMaps.push(listenerMap);
return <div />;
}
ReactDOM.render(<Test />, container);
ReactDOM.render(<Test />, container);
expect(listenerMaps.length).toEqual(2);
expect(listenerMaps[0]).toEqual(listenerMaps[1]);
});
});
});
@@ -17,6 +17,8 @@ import type {
ReactEventResponderListener,
} from 'shared/ReactTypes';
import type {SuspenseConfig} from 'react-reconciler/src/ReactFiberSuspenseConfig';
import type {ReactDOMListenerMap} from 'shared/ReactDOMTypes';
import {validateContextBounds} from './ReactPartialRendererContext';
import invariant from 'shared/invariant';
@@ -474,6 +476,13 @@ function useTransition(
return [startTransition, false];
}
function useEvent(event: any): ReactDOMListenerMap {
return {
clear: noop,
setListener: noop,
};
}
function noop(): void {}
export let currentThreadID: ThreadID = 0;
@@ -500,4 +509,5 @@ export const Dispatcher: DispatcherType = {
useResponder,
useDeferredValue,
useTransition,
useEvent,
};
@@ -76,6 +76,10 @@ export type UpdatePayload = Object;
export type TimeoutHandle = TimeoutID;
export type NoTimeout = -1;
export type ReactListenerEvent = Object;
export type ReactListenerMap = Object;
export type ReactListener = Object;
// TODO: Remove this conditional once all changes have propagated.
if (registerEventHandler) {
/**
@@ -26,6 +26,10 @@ import ReactNativeFiberHostComponent from './ReactNativeFiberHostComponent';
const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
export type ReactListenerEvent = Object;
export type ReactListenerMap = Object;
export type ReactListener = Object;
export type Type = string;
export type Props = Object;
export type Container = number;
+122 -1
View File
@@ -17,8 +17,13 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {HookEffectTag} from './ReactHookEffectTags';
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
import type {
ReactListenerEvent,
ReactListenerMap,
} from './ReactFiberHostConfig';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
import {NoWork, Sync} from './ReactFiberExpirationTime';
import {readContext} from './ReactFiberNewContext';
@@ -28,6 +33,7 @@ import {
Passive as PassiveEffect,
} from 'shared/ReactSideEffectTags';
import {
NoEffect as NoHookEffect,
HasEffect as HookHasEffect,
Layout as HookLayout,
Passive as HookPassive,
@@ -97,6 +103,7 @@ export type Dispatcher = {|
useTransition(
config: SuspenseConfig | void | null,
): [(() => void) => void, boolean],
useEvent(event: ReactListenerEvent): ReactListenerMap,
|};
type Update<S, A> = {|
@@ -129,7 +136,8 @@ export type HookType =
| 'useDebugValue'
| 'useResponder'
| 'useDeferredValue'
| 'useTransition';
| 'useTransition'
| 'useEvent';
let didWarnAboutMismatchedHooksForComponent;
if (__DEV__) {
@@ -1369,6 +1377,77 @@ function dispatchAction<S, A>(
}
}
const noOpMount = () => {};
function mountEventListener(event: ReactListenerEvent): ReactListenerMap {
if (enableUseEventAPI) {
const hook = mountWorkInProgressHook();
const clear = () => {
// TODO
};
const reactListenerMap: ReactListenerMap = {
clear,
setListener(instance: EventTarget, callback: ?(Event) => void): void {
// TODO
},
};
// In order to clear up upon the hook unmounting,
// we ensure we set the effecrt tag so that we visit
// this effect in the commit phase, so we can handle
// clean-up accordingly.
currentlyRenderingFiber.effectTag |= UpdateEffect;
pushEffect(NoHookEffect, noOpMount, clear, null);
hook.memoizedState = [reactListenerMap, event, clear];
return reactListenerMap;
}
// To make Flow not complain
return (undefined: any);
}
function updateEventListener(event: ReactListenerEvent): ReactListenerMap {
if (enableUseEventAPI) {
const hook = updateWorkInProgressHook();
const [reactListenerMap, memoizedEvent, clear] = hook.memoizedState;
if (__DEV__) {
if (memoizedEvent.type !== event.type) {
console.warn(
'The event type argument passed to the useEvent() hook was different between renders.' +
' The event type is static and should never change between renders.',
);
}
if (memoizedEvent.capture !== event.capture) {
console.warn(
'The "capture" option passed to the useEvent() hook was different between renders.' +
' The "capture" option is static and should never change between renders.',
);
}
if (memoizedEvent.priority !== event.priority) {
console.warn(
'The "priority" option passed to the useEvent() hook was different between renders.' +
' The "priority" option is static and should never change between renders.',
);
}
if (memoizedEvent.passive !== event.passive) {
console.warn(
'The "passive" option passed to the useEvent() hook was different between renders.' +
' The "passive" option is static and should never change between renders.',
);
}
}
// In order to clear up upon the hook unmounting,
// we ensure we set the effecrt tag so that we visit
// this effect in the commit phase, so we can handle
// clean-up accordingly.
currentlyRenderingFiber.effectTag |= UpdateEffect;
pushEffect(NoHookEffect, noOpMount, clear, null);
return reactListenerMap;
}
// To make Flow not complain
return (undefined: any);
}
export const ContextOnlyDispatcher: Dispatcher = {
readContext,
@@ -1385,6 +1464,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
useResponder: throwInvalidHookError,
useDeferredValue: throwInvalidHookError,
useTransition: throwInvalidHookError,
useEvent: throwInvalidHookError,
};
const HooksDispatcherOnMount: Dispatcher = {
@@ -1403,6 +1483,7 @@ const HooksDispatcherOnMount: Dispatcher = {
useResponder: createDeprecatedResponderListener,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useEvent: mountEventListener,
};
const HooksDispatcherOnUpdate: Dispatcher = {
@@ -1421,6 +1502,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
useResponder: createDeprecatedResponderListener,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useEvent: updateEventListener,
};
const HooksDispatcherOnRerender: Dispatcher = {
@@ -1439,6 +1521,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
useResponder: createDeprecatedResponderListener,
useDeferredValue: rerenderDeferredValue,
useTransition: rerenderTransition,
useEvent: updateEventListener,
};
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
@@ -1588,6 +1671,11 @@ if (__DEV__) {
mountHookTypesDev();
return mountTransition(config);
},
useEvent(event: ReactListenerEvent): ReactListenerMap {
currentHookNameInDev = 'useEvent';
mountHookTypesDev();
return mountEventListener(event);
},
};
HooksDispatcherOnMountWithHookTypesInDEV = {
@@ -1705,6 +1793,11 @@ if (__DEV__) {
updateHookTypesDev();
return mountTransition(config);
},
useEvent(event: ReactListenerEvent): ReactListenerMap {
currentHookNameInDev = 'useEvent';
updateHookTypesDev();
return mountEventListener(event);
},
};
HooksDispatcherOnUpdateInDEV = {
@@ -1822,6 +1915,11 @@ if (__DEV__) {
updateHookTypesDev();
return updateTransition(config);
},
useEvent(event: ReactListenerEvent): ReactListenerMap {
currentHookNameInDev = 'useEvent';
updateHookTypesDev();
return updateEventListener(event);
},
};
HooksDispatcherOnRerenderInDEV = {
@@ -1939,6 +2037,11 @@ if (__DEV__) {
updateHookTypesDev();
return rerenderTransition(config);
},
useEvent(event: ReactListenerEvent): ReactListenerMap {
currentHookNameInDev = 'useEvent';
updateHookTypesDev();
return updateEventListener(event);
},
};
InvalidNestedHooksDispatcherOnMountInDEV = {
@@ -2070,6 +2173,12 @@ if (__DEV__) {
mountHookTypesDev();
return mountTransition(config);
},
useEvent(event: ReactListenerEvent): ReactListenerMap {
currentHookNameInDev = 'useEvent';
warnInvalidHookAccess();
mountHookTypesDev();
return mountEventListener(event);
},
};
InvalidNestedHooksDispatcherOnUpdateInDEV = {
@@ -2201,6 +2310,12 @@ if (__DEV__) {
updateHookTypesDev();
return updateTransition(config);
},
useEvent(event: ReactListenerEvent): ReactListenerMap {
currentHookNameInDev = 'useEvent';
warnInvalidHookAccess();
updateHookTypesDev();
return updateEventListener(event);
},
};
InvalidNestedHooksDispatcherOnRerenderInDEV = {
@@ -2332,5 +2447,11 @@ if (__DEV__) {
updateHookTypesDev();
return rerenderTransition(config);
},
useEvent(event: ReactListenerEvent): ReactListenerMap {
currentHookNameInDev = 'useEvent';
warnInvalidHookAccess();
updateHookTypesDev();
return updateEventListener(event);
},
};
}
@@ -38,6 +38,9 @@ export opaque type ChildSet = mixed; // eslint-disable-line no-undef
export opaque type TimeoutHandle = mixed; // eslint-disable-line no-undef
export opaque type NoTimeout = mixed; // eslint-disable-line no-undef
export type EventResponder = any;
export type ReactListenerEvent = Object;
export type ReactListenerMap = Object;
export type ReactListener = Object;
export const getPublicInstance = $$$hostConfig.getPublicInstance;
export const getRootHostContext = $$$hostConfig.getRootHostContext;
@@ -45,6 +45,10 @@ export type TimeoutHandle = TimeoutID;
export type NoTimeout = -1;
export type EventResponder = any;
export type ReactListenerEvent = Object;
export type ReactListenerMap = Object;
export type ReactListener = Object;
export * from 'shared/HostConfigWithNoPersistence';
export * from 'shared/HostConfigWithNoHydration';