mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
experimental_useEvent (#25229)
This commit adds a new hook `useEvent` per the RFC [here](https://github.com/reactjs/rfcs/pull/220), gated as experimental. Co-authored-by: Rick Hanlon <[email protected]> Co-authored-by: Rick Hanlon <[email protected]> Co-authored-by: Lauren Tan <[email protected]>
This commit is contained in:
committed by
GitHub
co-authored by
Rick Hanlon
Rick Hanlon
parent
3e745d92b6
commit
9329254ab5
@@ -48,6 +48,7 @@ import {
|
||||
enableUpdaterTracking,
|
||||
enableCache,
|
||||
enableTransitionTracing,
|
||||
enableUseEventHook,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
FunctionComponent,
|
||||
@@ -162,6 +163,7 @@ import {
|
||||
Layout as HookLayout,
|
||||
Insertion as HookInsertion,
|
||||
Passive as HookPassive,
|
||||
Snapshot as HookSnapshot,
|
||||
} from './ReactHookEffectTags';
|
||||
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new';
|
||||
import {doesFiberContain} from './ReactFiberTreeReflection';
|
||||
@@ -407,14 +409,24 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
|
||||
|
||||
if ((flags & Snapshot) !== NoFlags) {
|
||||
setCurrentDebugFiberInDEV(finishedWork);
|
||||
}
|
||||
|
||||
switch (finishedWork.tag) {
|
||||
case FunctionComponent:
|
||||
case ForwardRef:
|
||||
case SimpleMemoComponent: {
|
||||
break;
|
||||
switch (finishedWork.tag) {
|
||||
case FunctionComponent: {
|
||||
if (enableUseEventHook) {
|
||||
if ((flags & Update) !== NoFlags) {
|
||||
// useEvent doesn't need to be cleaned up
|
||||
commitHookEffectListMount(HookSnapshot | HookHasEffect, finishedWork);
|
||||
}
|
||||
}
|
||||
case ClassComponent: {
|
||||
break;
|
||||
}
|
||||
case ForwardRef:
|
||||
case SimpleMemoComponent: {
|
||||
break;
|
||||
}
|
||||
case ClassComponent: {
|
||||
if ((flags & Snapshot) !== NoFlags) {
|
||||
if (current !== null) {
|
||||
const prevProps = current.memoizedProps;
|
||||
const prevState = current.memoizedState;
|
||||
@@ -468,29 +480,35 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
|
||||
}
|
||||
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HostRoot: {
|
||||
break;
|
||||
}
|
||||
case HostRoot: {
|
||||
if ((flags & Snapshot) !== NoFlags) {
|
||||
if (supportsMutation) {
|
||||
const root = finishedWork.stateNode;
|
||||
clearContainer(root.containerInfo);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HostComponent:
|
||||
case HostText:
|
||||
case HostPortal:
|
||||
case IncompleteClassComponent:
|
||||
// Nothing to do for these component types
|
||||
break;
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
case HostComponent:
|
||||
case HostText:
|
||||
case HostPortal:
|
||||
case IncompleteClassComponent:
|
||||
// Nothing to do for these component types
|
||||
break;
|
||||
default: {
|
||||
if ((flags & Snapshot) !== NoFlags) {
|
||||
throw new Error(
|
||||
'This unit of work tag should not have side-effects. This error is ' +
|
||||
'likely caused by a bug in React. Please file an issue.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((flags & Snapshot) !== NoFlags) {
|
||||
resetCurrentDebugFiberInDEV();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
enableUpdaterTracking,
|
||||
enableCache,
|
||||
enableTransitionTracing,
|
||||
enableUseEventHook,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
FunctionComponent,
|
||||
@@ -162,6 +163,7 @@ import {
|
||||
Layout as HookLayout,
|
||||
Insertion as HookInsertion,
|
||||
Passive as HookPassive,
|
||||
Snapshot as HookSnapshot,
|
||||
} from './ReactHookEffectTags';
|
||||
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old';
|
||||
import {doesFiberContain} from './ReactFiberTreeReflection';
|
||||
@@ -407,14 +409,24 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
|
||||
|
||||
if ((flags & Snapshot) !== NoFlags) {
|
||||
setCurrentDebugFiberInDEV(finishedWork);
|
||||
}
|
||||
|
||||
switch (finishedWork.tag) {
|
||||
case FunctionComponent:
|
||||
case ForwardRef:
|
||||
case SimpleMemoComponent: {
|
||||
break;
|
||||
switch (finishedWork.tag) {
|
||||
case FunctionComponent: {
|
||||
if (enableUseEventHook) {
|
||||
if ((flags & Update) !== NoFlags) {
|
||||
// useEvent doesn't need to be cleaned up
|
||||
commitHookEffectListMount(HookSnapshot | HookHasEffect, finishedWork);
|
||||
}
|
||||
}
|
||||
case ClassComponent: {
|
||||
break;
|
||||
}
|
||||
case ForwardRef:
|
||||
case SimpleMemoComponent: {
|
||||
break;
|
||||
}
|
||||
case ClassComponent: {
|
||||
if ((flags & Snapshot) !== NoFlags) {
|
||||
if (current !== null) {
|
||||
const prevProps = current.memoizedProps;
|
||||
const prevState = current.memoizedState;
|
||||
@@ -468,29 +480,35 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
|
||||
}
|
||||
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HostRoot: {
|
||||
break;
|
||||
}
|
||||
case HostRoot: {
|
||||
if ((flags & Snapshot) !== NoFlags) {
|
||||
if (supportsMutation) {
|
||||
const root = finishedWork.stateNode;
|
||||
clearContainer(root.containerInfo);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HostComponent:
|
||||
case HostText:
|
||||
case HostPortal:
|
||||
case IncompleteClassComponent:
|
||||
// Nothing to do for these component types
|
||||
break;
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
case HostComponent:
|
||||
case HostText:
|
||||
case HostPortal:
|
||||
case IncompleteClassComponent:
|
||||
// Nothing to do for these component types
|
||||
break;
|
||||
default: {
|
||||
if ((flags & Snapshot) !== NoFlags) {
|
||||
throw new Error(
|
||||
'This unit of work tag should not have side-effects. This error is ' +
|
||||
'likely caused by a bug in React. Please file an issue.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((flags & Snapshot) !== NoFlags) {
|
||||
resetCurrentDebugFiberInDEV();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
enableTransitionTracing,
|
||||
enableUseHook,
|
||||
enableUseMemoCacheHook,
|
||||
enableUseEventHook,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
REACT_CONTEXT_TYPE,
|
||||
@@ -85,6 +86,7 @@ import {
|
||||
Layout as HookLayout,
|
||||
Passive as HookPassive,
|
||||
Insertion as HookInsertion,
|
||||
Snapshot as HookSnapshot,
|
||||
} from './ReactHookEffectTags';
|
||||
import {
|
||||
getWorkInProgressRoot,
|
||||
@@ -93,6 +95,7 @@ import {
|
||||
requestUpdateLane,
|
||||
requestEventTime,
|
||||
markSkippedUpdateLanes,
|
||||
isInvalidExecutionContextForEventFunction,
|
||||
} from './ReactFiberWorkLoop.new';
|
||||
|
||||
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
|
||||
@@ -1868,6 +1871,49 @@ function updateEffect(
|
||||
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
|
||||
}
|
||||
|
||||
function mountEvent<T>(callback: () => T): () => T {
|
||||
const hook = mountWorkInProgressHook();
|
||||
const ref = {current: callback};
|
||||
|
||||
function event() {
|
||||
if (isInvalidExecutionContextForEventFunction()) {
|
||||
throw new Error('An event from useEvent was called during render.');
|
||||
}
|
||||
return ref.current.apply(undefined, arguments);
|
||||
}
|
||||
|
||||
// TODO: We don't need all the overhead of an effect object since there are no deps and no
|
||||
// clean up functions.
|
||||
mountEffectImpl(
|
||||
UpdateEffect,
|
||||
HookSnapshot,
|
||||
() => {
|
||||
ref.current = callback;
|
||||
},
|
||||
[ref, callback],
|
||||
);
|
||||
|
||||
hook.memoizedState = [ref, event];
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
function updateEvent<T>(callback: () => T): () => T {
|
||||
const hook = updateWorkInProgressHook();
|
||||
const ref = hook.memoizedState[0];
|
||||
|
||||
updateEffectImpl(
|
||||
UpdateEffect,
|
||||
HookSnapshot,
|
||||
() => {
|
||||
ref.current = callback;
|
||||
},
|
||||
[ref, callback],
|
||||
);
|
||||
|
||||
return hook.memoizedState[1];
|
||||
}
|
||||
|
||||
function mountInsertionEffect(
|
||||
create: () => (() => void) | void,
|
||||
deps: Array<mixed> | void | null,
|
||||
@@ -2546,6 +2592,9 @@ if (enableUseHook) {
|
||||
if (enableUseMemoCacheHook) {
|
||||
(ContextOnlyDispatcher: Dispatcher).useMemoCache = throwInvalidHookError;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(ContextOnlyDispatcher: Dispatcher).useEvent = throwInvalidHookError;
|
||||
}
|
||||
|
||||
const HooksDispatcherOnMount: Dispatcher = {
|
||||
readContext,
|
||||
@@ -2581,6 +2630,9 @@ if (enableUseHook) {
|
||||
if (enableUseMemoCacheHook) {
|
||||
(HooksDispatcherOnMount: Dispatcher).useMemoCache = useMemoCache;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(HooksDispatcherOnMount: Dispatcher).useEvent = mountEvent;
|
||||
}
|
||||
const HooksDispatcherOnUpdate: Dispatcher = {
|
||||
readContext,
|
||||
|
||||
@@ -2614,6 +2666,9 @@ if (enableUseMemoCacheHook) {
|
||||
if (enableUseHook) {
|
||||
(HooksDispatcherOnUpdate: Dispatcher).use = use;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(HooksDispatcherOnUpdate: Dispatcher).useEvent = updateEvent;
|
||||
}
|
||||
|
||||
const HooksDispatcherOnRerender: Dispatcher = {
|
||||
readContext,
|
||||
@@ -2648,6 +2703,9 @@ if (enableUseHook) {
|
||||
if (enableUseMemoCacheHook) {
|
||||
(HooksDispatcherOnRerender: Dispatcher).useMemoCache = useMemoCache;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(HooksDispatcherOnRerender: Dispatcher).useEvent = updateEvent;
|
||||
}
|
||||
|
||||
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
|
||||
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
|
||||
@@ -2829,6 +2887,15 @@ if (__DEV__) {
|
||||
if (enableUseMemoCacheHook) {
|
||||
(HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(HooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent<T>(
|
||||
callback: () => T,
|
||||
): () => T {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
mountHookTypesDev();
|
||||
return mountEvent(callback);
|
||||
};
|
||||
}
|
||||
|
||||
HooksDispatcherOnMountWithHookTypesInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
@@ -2977,6 +3044,15 @@ if (__DEV__) {
|
||||
if (enableUseMemoCacheHook) {
|
||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useMemoCache = useMemoCache;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useEvent = function useEvent<
|
||||
T,
|
||||
>(callback: () => T): () => T {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
updateHookTypesDev();
|
||||
return mountEvent(callback);
|
||||
};
|
||||
}
|
||||
|
||||
HooksDispatcherOnUpdateInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
@@ -3125,6 +3201,15 @@ if (__DEV__) {
|
||||
if (enableUseMemoCacheHook) {
|
||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent<T>(
|
||||
callback: () => T,
|
||||
): () => T {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
updateHookTypesDev();
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
|
||||
HooksDispatcherOnRerenderInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
@@ -3274,6 +3359,15 @@ if (__DEV__) {
|
||||
if (enableUseMemoCacheHook) {
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = useMemoCache;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent<
|
||||
T,
|
||||
>(callback: () => T): () => T {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
updateHookTypesDev();
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
|
||||
InvalidNestedHooksDispatcherOnMountInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
@@ -3449,6 +3543,16 @@ if (__DEV__) {
|
||||
return useMemoCache(size);
|
||||
};
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent<
|
||||
T,
|
||||
>(callback: () => T): () => T {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
warnInvalidHookAccess();
|
||||
mountHookTypesDev();
|
||||
return mountEvent(callback);
|
||||
};
|
||||
}
|
||||
|
||||
InvalidNestedHooksDispatcherOnUpdateInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
@@ -3624,6 +3728,16 @@ if (__DEV__) {
|
||||
return useMemoCache(size);
|
||||
};
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent<
|
||||
T,
|
||||
>(callback: () => T): () => T {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
|
||||
InvalidNestedHooksDispatcherOnRerenderInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
@@ -3800,4 +3914,14 @@ if (__DEV__) {
|
||||
return useMemoCache(size);
|
||||
};
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent<
|
||||
T,
|
||||
>(callback: () => T): () => T {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
enableTransitionTracing,
|
||||
enableUseHook,
|
||||
enableUseMemoCacheHook,
|
||||
enableUseEventHook,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
REACT_CONTEXT_TYPE,
|
||||
@@ -85,6 +86,7 @@ import {
|
||||
Layout as HookLayout,
|
||||
Passive as HookPassive,
|
||||
Insertion as HookInsertion,
|
||||
Snapshot as HookSnapshot,
|
||||
} from './ReactHookEffectTags';
|
||||
import {
|
||||
getWorkInProgressRoot,
|
||||
@@ -93,6 +95,7 @@ import {
|
||||
requestUpdateLane,
|
||||
requestEventTime,
|
||||
markSkippedUpdateLanes,
|
||||
isInvalidExecutionContextForEventFunction,
|
||||
} from './ReactFiberWorkLoop.old';
|
||||
|
||||
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
|
||||
@@ -1868,6 +1871,49 @@ function updateEffect(
|
||||
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
|
||||
}
|
||||
|
||||
function mountEvent<T>(callback: () => T): () => T {
|
||||
const hook = mountWorkInProgressHook();
|
||||
const ref = {current: callback};
|
||||
|
||||
function event() {
|
||||
if (isInvalidExecutionContextForEventFunction()) {
|
||||
throw new Error('An event from useEvent was called during render.');
|
||||
}
|
||||
return ref.current.apply(undefined, arguments);
|
||||
}
|
||||
|
||||
// TODO: We don't need all the overhead of an effect object since there are no deps and no
|
||||
// clean up functions.
|
||||
mountEffectImpl(
|
||||
UpdateEffect,
|
||||
HookSnapshot,
|
||||
() => {
|
||||
ref.current = callback;
|
||||
},
|
||||
[ref, callback],
|
||||
);
|
||||
|
||||
hook.memoizedState = [ref, event];
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
function updateEvent<T>(callback: () => T): () => T {
|
||||
const hook = updateWorkInProgressHook();
|
||||
const ref = hook.memoizedState[0];
|
||||
|
||||
updateEffectImpl(
|
||||
UpdateEffect,
|
||||
HookSnapshot,
|
||||
() => {
|
||||
ref.current = callback;
|
||||
},
|
||||
[ref, callback],
|
||||
);
|
||||
|
||||
return hook.memoizedState[1];
|
||||
}
|
||||
|
||||
function mountInsertionEffect(
|
||||
create: () => (() => void) | void,
|
||||
deps: Array<mixed> | void | null,
|
||||
@@ -2546,6 +2592,9 @@ if (enableUseHook) {
|
||||
if (enableUseMemoCacheHook) {
|
||||
(ContextOnlyDispatcher: Dispatcher).useMemoCache = throwInvalidHookError;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(ContextOnlyDispatcher: Dispatcher).useEvent = throwInvalidHookError;
|
||||
}
|
||||
|
||||
const HooksDispatcherOnMount: Dispatcher = {
|
||||
readContext,
|
||||
@@ -2581,6 +2630,9 @@ if (enableUseHook) {
|
||||
if (enableUseMemoCacheHook) {
|
||||
(HooksDispatcherOnMount: Dispatcher).useMemoCache = useMemoCache;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(HooksDispatcherOnMount: Dispatcher).useEvent = mountEvent;
|
||||
}
|
||||
const HooksDispatcherOnUpdate: Dispatcher = {
|
||||
readContext,
|
||||
|
||||
@@ -2614,6 +2666,9 @@ if (enableUseMemoCacheHook) {
|
||||
if (enableUseHook) {
|
||||
(HooksDispatcherOnUpdate: Dispatcher).use = use;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(HooksDispatcherOnUpdate: Dispatcher).useEvent = updateEvent;
|
||||
}
|
||||
|
||||
const HooksDispatcherOnRerender: Dispatcher = {
|
||||
readContext,
|
||||
@@ -2648,6 +2703,9 @@ if (enableUseHook) {
|
||||
if (enableUseMemoCacheHook) {
|
||||
(HooksDispatcherOnRerender: Dispatcher).useMemoCache = useMemoCache;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(HooksDispatcherOnRerender: Dispatcher).useEvent = updateEvent;
|
||||
}
|
||||
|
||||
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
|
||||
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
|
||||
@@ -2829,6 +2887,15 @@ if (__DEV__) {
|
||||
if (enableUseMemoCacheHook) {
|
||||
(HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(HooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent<T>(
|
||||
callback: () => T,
|
||||
): () => T {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
mountHookTypesDev();
|
||||
return mountEvent(callback);
|
||||
};
|
||||
}
|
||||
|
||||
HooksDispatcherOnMountWithHookTypesInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
@@ -2977,6 +3044,15 @@ if (__DEV__) {
|
||||
if (enableUseMemoCacheHook) {
|
||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useMemoCache = useMemoCache;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useEvent = function useEvent<
|
||||
T,
|
||||
>(callback: () => T): () => T {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
updateHookTypesDev();
|
||||
return mountEvent(callback);
|
||||
};
|
||||
}
|
||||
|
||||
HooksDispatcherOnUpdateInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
@@ -3125,6 +3201,15 @@ if (__DEV__) {
|
||||
if (enableUseMemoCacheHook) {
|
||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(HooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent<T>(
|
||||
callback: () => T,
|
||||
): () => T {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
updateHookTypesDev();
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
|
||||
HooksDispatcherOnRerenderInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
@@ -3274,6 +3359,15 @@ if (__DEV__) {
|
||||
if (enableUseMemoCacheHook) {
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = useMemoCache;
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(HooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent<
|
||||
T,
|
||||
>(callback: () => T): () => T {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
updateHookTypesDev();
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
|
||||
InvalidNestedHooksDispatcherOnMountInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
@@ -3449,6 +3543,16 @@ if (__DEV__) {
|
||||
return useMemoCache(size);
|
||||
};
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent<
|
||||
T,
|
||||
>(callback: () => T): () => T {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
warnInvalidHookAccess();
|
||||
mountHookTypesDev();
|
||||
return mountEvent(callback);
|
||||
};
|
||||
}
|
||||
|
||||
InvalidNestedHooksDispatcherOnUpdateInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
@@ -3624,6 +3728,16 @@ if (__DEV__) {
|
||||
return useMemoCache(size);
|
||||
};
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent<
|
||||
T,
|
||||
>(callback: () => T): () => T {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
|
||||
InvalidNestedHooksDispatcherOnRerenderInDEV = {
|
||||
readContext<T>(context: ReactContext<T>): T {
|
||||
@@ -3800,4 +3914,14 @@ if (__DEV__) {
|
||||
return useMemoCache(size);
|
||||
};
|
||||
}
|
||||
if (enableUseEventHook) {
|
||||
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent<
|
||||
T,
|
||||
>(callback: () => T): () => T {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateEvent(callback);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1605,6 +1605,11 @@ export function isAlreadyRendering(): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isInvalidExecutionContextForEventFunction() {
|
||||
// Used to throw if certain APIs are called from the wrong context.
|
||||
return (executionContext & RenderContext) !== NoContext;
|
||||
}
|
||||
|
||||
export function flushControlled(fn: () => mixed): void {
|
||||
const prevExecutionContext = executionContext;
|
||||
executionContext |= BatchedContext;
|
||||
|
||||
@@ -1605,6 +1605,11 @@ export function isAlreadyRendering(): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isInvalidExecutionContextForEventFunction() {
|
||||
// Used to throw if certain APIs are called from the wrong context.
|
||||
return (executionContext & RenderContext) !== NoContext;
|
||||
}
|
||||
|
||||
export function flushControlled(fn: () => mixed): void {
|
||||
const prevExecutionContext = executionContext;
|
||||
executionContext |= BatchedContext;
|
||||
|
||||
+6
-5
@@ -9,12 +9,13 @@
|
||||
|
||||
export type HookFlags = number;
|
||||
|
||||
export const NoFlags = /* */ 0b0000;
|
||||
export const NoFlags = /* */ 0b00000;
|
||||
|
||||
// Represents whether effect should fire.
|
||||
export const HasEffect = /* */ 0b0001;
|
||||
export const HasEffect = /* */ 0b00001;
|
||||
|
||||
// Represents the phase in which the effect (not the clean-up) fires.
|
||||
export const Insertion = /* */ 0b0010;
|
||||
export const Layout = /* */ 0b0100;
|
||||
export const Passive = /* */ 0b1000;
|
||||
export const Snapshot = /* */ 0b00010;
|
||||
export const Insertion = /* */ 0b00100;
|
||||
export const Layout = /* */ 0b01000;
|
||||
export const Passive = /* */ 0b10000;
|
||||
|
||||
@@ -42,6 +42,7 @@ export type HookType =
|
||||
| 'useContext'
|
||||
| 'useRef'
|
||||
| 'useEffect'
|
||||
| 'useEvent'
|
||||
| 'useInsertionEffect'
|
||||
| 'useLayoutEffect'
|
||||
| 'useCallback'
|
||||
@@ -376,6 +377,7 @@ export type Dispatcher = {
|
||||
create: () => (() => void) | void,
|
||||
deps: Array<mixed> | void | null,
|
||||
): void,
|
||||
useEvent?: <T>(callback: () => T) => () => T,
|
||||
useInsertionEffect(
|
||||
create: () => (() => void) | void,
|
||||
deps: Array<mixed> | void | null,
|
||||
|
||||
@@ -0,0 +1,668 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
/* eslint-disable no-func-assign */
|
||||
|
||||
'use strict';
|
||||
|
||||
import {useInsertionEffect} from 'react';
|
||||
|
||||
describe('useEvent', () => {
|
||||
let React;
|
||||
let ReactNoop;
|
||||
let Scheduler;
|
||||
let act;
|
||||
let createContext;
|
||||
let useContext;
|
||||
let useState;
|
||||
let useEvent;
|
||||
let useEffect;
|
||||
let useLayoutEffect;
|
||||
let useMemo;
|
||||
|
||||
beforeEach(() => {
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
Scheduler = require('scheduler');
|
||||
|
||||
act = require('jest-react').act;
|
||||
createContext = React.createContext;
|
||||
useContext = React.useContext;
|
||||
useState = React.useState;
|
||||
useEvent = React.experimental_useEvent;
|
||||
useEffect = React.useEffect;
|
||||
useLayoutEffect = React.useLayoutEffect;
|
||||
useMemo = React.useMemo;
|
||||
});
|
||||
|
||||
function span(prop) {
|
||||
return {type: 'span', hidden: false, children: [], prop};
|
||||
}
|
||||
|
||||
function Text(props) {
|
||||
Scheduler.unstable_yieldValue(props.text);
|
||||
return <span prop={props.text} />;
|
||||
}
|
||||
|
||||
// @gate enableUseEventHook
|
||||
it('memoizes basic case correctly', () => {
|
||||
class IncrementButton extends React.PureComponent {
|
||||
increment = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
render() {
|
||||
return <Text text="Increment" />;
|
||||
}
|
||||
}
|
||||
|
||||
function Counter({incrementBy}) {
|
||||
const [count, updateCount] = useState(0);
|
||||
const onClick = useEvent(() => updateCount(c => c + incrementBy));
|
||||
|
||||
return (
|
||||
<>
|
||||
<IncrementButton onClick={() => onClick()} ref={button} />
|
||||
<Text text={'Count: ' + count} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const button = React.createRef(null);
|
||||
ReactNoop.render(<Counter incrementBy={1} />);
|
||||
expect(Scheduler).toFlushAndYield(['Increment', 'Count: 0']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 0'),
|
||||
]);
|
||||
|
||||
act(button.current.increment);
|
||||
expect(Scheduler).toHaveYielded(['Increment', 'Count: 1']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 1'),
|
||||
]);
|
||||
|
||||
act(button.current.increment);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Increment',
|
||||
// Event should use the updated callback function closed over the new value.
|
||||
'Count: 2',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 2'),
|
||||
]);
|
||||
|
||||
// Increase the increment prop amount
|
||||
ReactNoop.render(<Counter incrementBy={10} />);
|
||||
expect(Scheduler).toFlushAndYield(['Increment', 'Count: 2']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 2'),
|
||||
]);
|
||||
|
||||
// Event uses the new prop
|
||||
act(button.current.increment);
|
||||
expect(Scheduler).toHaveYielded(['Increment', 'Count: 12']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 12'),
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate enableUseEventHook
|
||||
it('does not preserve `this` in event functions', () => {
|
||||
class GreetButton extends React.PureComponent {
|
||||
greet = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
render() {
|
||||
return <Text text={'Say ' + this.props.hello} />;
|
||||
}
|
||||
}
|
||||
function Greeter({hello}) {
|
||||
const person = {
|
||||
toString() {
|
||||
return 'Jane';
|
||||
},
|
||||
greet() {
|
||||
return updateGreeting(this + ' says ' + hello);
|
||||
},
|
||||
};
|
||||
const [greeting, updateGreeting] = useState('Seb says ' + hello);
|
||||
const onClick = useEvent(person.greet);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GreetButton hello={hello} onClick={() => onClick()} ref={button} />
|
||||
<Text text={'Greeting: ' + greeting} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const button = React.createRef(null);
|
||||
ReactNoop.render(<Greeter hello={'hej'} />);
|
||||
expect(Scheduler).toFlushAndYield(['Say hej', 'Greeting: Seb says hej']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Say hej'),
|
||||
span('Greeting: Seb says hej'),
|
||||
]);
|
||||
|
||||
act(button.current.greet);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Say hej',
|
||||
'Greeting: undefined says hej',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Say hej'),
|
||||
span('Greeting: undefined says hej'),
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate enableUseEventHook
|
||||
it('throws when called in render', () => {
|
||||
class IncrementButton extends React.PureComponent {
|
||||
increment = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
|
||||
render() {
|
||||
// Will throw.
|
||||
this.props.onClick();
|
||||
|
||||
return <Text text="Increment" />;
|
||||
}
|
||||
}
|
||||
|
||||
function Counter({incrementBy}) {
|
||||
const [count, updateCount] = useState(0);
|
||||
const onClick = useEvent(() => updateCount(c => c + incrementBy));
|
||||
|
||||
return (
|
||||
<>
|
||||
<IncrementButton onClick={() => onClick()} />
|
||||
<Text text={'Count: ' + count} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ReactNoop.render(<Counter incrementBy={1} />);
|
||||
expect(Scheduler).toFlushAndThrow(
|
||||
'An event from useEvent was called during render',
|
||||
);
|
||||
|
||||
// If something throws, we try one more time synchronously in case the error was
|
||||
// caused by a data race. See recoverFromConcurrentError
|
||||
expect(Scheduler).toHaveYielded(['Count: 0', 'Count: 0']);
|
||||
});
|
||||
|
||||
// @gate enableUseEventHook
|
||||
it("useLayoutEffect shouldn't re-fire when event handlers change", () => {
|
||||
class IncrementButton extends React.PureComponent {
|
||||
increment = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
render() {
|
||||
return <Text text="Increment" />;
|
||||
}
|
||||
}
|
||||
|
||||
function Counter({incrementBy}) {
|
||||
const [count, updateCount] = useState(0);
|
||||
const increment = useEvent(amount =>
|
||||
updateCount(c => c + (amount || incrementBy)),
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
Scheduler.unstable_yieldValue('Effect: by ' + incrementBy * 2);
|
||||
increment(incrementBy * 2);
|
||||
}, [incrementBy]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IncrementButton onClick={() => increment()} ref={button} />
|
||||
<Text text={'Count: ' + count} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const button = React.createRef(null);
|
||||
ReactNoop.render(<Counter incrementBy={1} />);
|
||||
expect(Scheduler).toHaveYielded([]);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Increment',
|
||||
'Count: 0',
|
||||
'Effect: by 2',
|
||||
'Increment',
|
||||
'Count: 2',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 2'),
|
||||
]);
|
||||
|
||||
act(button.current.increment);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Increment',
|
||||
// Effect should not re-run because the dependency hasn't changed.
|
||||
'Count: 3',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 3'),
|
||||
]);
|
||||
|
||||
act(button.current.increment);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Increment',
|
||||
// Event should use the updated callback function closed over the new value.
|
||||
'Count: 4',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 4'),
|
||||
]);
|
||||
|
||||
// Increase the increment prop amount
|
||||
ReactNoop.render(<Counter incrementBy={10} />);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Increment',
|
||||
'Count: 4',
|
||||
'Effect: by 20',
|
||||
'Increment',
|
||||
'Count: 24',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 24'),
|
||||
]);
|
||||
|
||||
// Event uses the new prop
|
||||
act(button.current.increment);
|
||||
expect(Scheduler).toHaveYielded(['Increment', 'Count: 34']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 34'),
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate enableUseEventHook
|
||||
it("useEffect shouldn't re-fire when event handlers change", () => {
|
||||
class IncrementButton extends React.PureComponent {
|
||||
increment = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
render() {
|
||||
return <Text text="Increment" />;
|
||||
}
|
||||
}
|
||||
|
||||
function Counter({incrementBy}) {
|
||||
const [count, updateCount] = useState(0);
|
||||
const increment = useEvent(amount =>
|
||||
updateCount(c => c + (amount || incrementBy)),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
Scheduler.unstable_yieldValue('Effect: by ' + incrementBy * 2);
|
||||
increment(incrementBy * 2);
|
||||
}, [incrementBy]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IncrementButton onClick={() => increment()} ref={button} />
|
||||
<Text text={'Count: ' + count} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const button = React.createRef(null);
|
||||
ReactNoop.render(<Counter incrementBy={1} />);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Increment',
|
||||
'Count: 0',
|
||||
'Effect: by 2',
|
||||
'Increment',
|
||||
'Count: 2',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 2'),
|
||||
]);
|
||||
|
||||
act(button.current.increment);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Increment',
|
||||
// Effect should not re-run because the dependency hasn't changed.
|
||||
'Count: 3',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 3'),
|
||||
]);
|
||||
|
||||
act(button.current.increment);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Increment',
|
||||
// Event should use the updated callback function closed over the new value.
|
||||
'Count: 4',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 4'),
|
||||
]);
|
||||
|
||||
// Increase the increment prop amount
|
||||
ReactNoop.render(<Counter incrementBy={10} />);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Increment',
|
||||
'Count: 4',
|
||||
'Effect: by 20',
|
||||
'Increment',
|
||||
'Count: 24',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 24'),
|
||||
]);
|
||||
|
||||
// Event uses the new prop
|
||||
act(button.current.increment);
|
||||
expect(Scheduler).toHaveYielded(['Increment', 'Count: 34']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 34'),
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate enableUseEventHook
|
||||
it('is stable in a custom hook', () => {
|
||||
class IncrementButton extends React.PureComponent {
|
||||
increment = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
render() {
|
||||
return <Text text="Increment" />;
|
||||
}
|
||||
}
|
||||
|
||||
function useCount(incrementBy) {
|
||||
const [count, updateCount] = useState(0);
|
||||
const increment = useEvent(amount =>
|
||||
updateCount(c => c + (amount || incrementBy)),
|
||||
);
|
||||
|
||||
return [count, increment];
|
||||
}
|
||||
|
||||
function Counter({incrementBy}) {
|
||||
const [count, increment] = useCount(incrementBy);
|
||||
|
||||
useEffect(() => {
|
||||
Scheduler.unstable_yieldValue('Effect: by ' + incrementBy * 2);
|
||||
increment(incrementBy * 2);
|
||||
}, [incrementBy]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IncrementButton onClick={() => increment()} ref={button} />
|
||||
<Text text={'Count: ' + count} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const button = React.createRef(null);
|
||||
ReactNoop.render(<Counter incrementBy={1} />);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Increment',
|
||||
'Count: 0',
|
||||
'Effect: by 2',
|
||||
'Increment',
|
||||
'Count: 2',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 2'),
|
||||
]);
|
||||
|
||||
act(button.current.increment);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Increment',
|
||||
// Effect should not re-run because the dependency hasn't changed.
|
||||
'Count: 3',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 3'),
|
||||
]);
|
||||
|
||||
act(button.current.increment);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Increment',
|
||||
// Event should use the updated callback function closed over the new value.
|
||||
'Count: 4',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 4'),
|
||||
]);
|
||||
|
||||
// Increase the increment prop amount
|
||||
ReactNoop.render(<Counter incrementBy={10} />);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Increment',
|
||||
'Count: 4',
|
||||
'Effect: by 20',
|
||||
'Increment',
|
||||
'Count: 24',
|
||||
]);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 24'),
|
||||
]);
|
||||
|
||||
// Event uses the new prop
|
||||
act(button.current.increment);
|
||||
expect(Scheduler).toHaveYielded(['Increment', 'Count: 34']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Increment'),
|
||||
span('Count: 34'),
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate enableUseEventHook
|
||||
it('is mutated before all other effects', () => {
|
||||
function Counter({value}) {
|
||||
useInsertionEffect(() => {
|
||||
Scheduler.unstable_yieldValue('Effect value: ' + value);
|
||||
increment();
|
||||
}, [value]);
|
||||
|
||||
// This is defined after the insertion effect, but it should
|
||||
// update the event fn _before_ the insertion effect fires.
|
||||
const increment = useEvent(() => {
|
||||
Scheduler.unstable_yieldValue('Event value: ' + value);
|
||||
});
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
ReactNoop.render(<Counter value={1} />);
|
||||
expect(Scheduler).toFlushAndYield(['Effect value: 1', 'Event value: 1']);
|
||||
|
||||
act(() => ReactNoop.render(<Counter value={2} />));
|
||||
expect(Scheduler).toHaveYielded(['Effect value: 2', 'Event value: 2']);
|
||||
});
|
||||
|
||||
// @gate enableUseEventHook
|
||||
it('integration: implements docs chat room example', () => {
|
||||
function createConnection() {
|
||||
let connectedCallback;
|
||||
let timeout;
|
||||
return {
|
||||
connect() {
|
||||
timeout = setTimeout(() => {
|
||||
if (connectedCallback) {
|
||||
connectedCallback();
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
on(event, callback) {
|
||||
if (connectedCallback) {
|
||||
throw Error('Cannot add the handler twice.');
|
||||
}
|
||||
if (event !== 'connected') {
|
||||
throw Error('Only "connected" event is supported.');
|
||||
}
|
||||
connectedCallback = callback;
|
||||
},
|
||||
disconnect() {
|
||||
clearTimeout(timeout);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function ChatRoom({roomId, theme}) {
|
||||
const onConnected = useEvent(() => {
|
||||
Scheduler.unstable_yieldValue('Connected! theme: ' + theme);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const connection = createConnection(roomId);
|
||||
connection.on('connected', () => {
|
||||
onConnected();
|
||||
});
|
||||
connection.connect();
|
||||
return () => connection.disconnect();
|
||||
}, [roomId, onConnected]);
|
||||
|
||||
return <Text text={`Welcome to the ${roomId} room!`} />;
|
||||
}
|
||||
|
||||
act(() => ReactNoop.render(<ChatRoom roomId="general" theme="light" />));
|
||||
expect(Scheduler).toHaveYielded(['Welcome to the general room!']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Welcome to the general room!'),
|
||||
]);
|
||||
|
||||
jest.advanceTimersByTime(100);
|
||||
Scheduler.unstable_advanceTime(100);
|
||||
expect(Scheduler).toHaveYielded(['Connected! theme: light']);
|
||||
|
||||
// change roomId only
|
||||
act(() => ReactNoop.render(<ChatRoom roomId="music" theme="light" />));
|
||||
expect(Scheduler).toHaveYielded(['Welcome to the music room!']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Welcome to the music room!'),
|
||||
]);
|
||||
jest.advanceTimersByTime(100);
|
||||
Scheduler.unstable_advanceTime(100);
|
||||
// should trigger a reconnect
|
||||
expect(Scheduler).toHaveYielded(['Connected! theme: light']);
|
||||
|
||||
// change theme only
|
||||
act(() => ReactNoop.render(<ChatRoom roomId="music" theme="dark" />));
|
||||
expect(Scheduler).toHaveYielded(['Welcome to the music room!']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Welcome to the music room!'),
|
||||
]);
|
||||
jest.advanceTimersByTime(100);
|
||||
Scheduler.unstable_advanceTime(100);
|
||||
// should not trigger a reconnect
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
|
||||
// change roomId only
|
||||
act(() => ReactNoop.render(<ChatRoom roomId="travel" theme="dark" />));
|
||||
expect(Scheduler).toHaveYielded(['Welcome to the travel room!']);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Welcome to the travel room!'),
|
||||
]);
|
||||
jest.advanceTimersByTime(100);
|
||||
Scheduler.unstable_advanceTime(100);
|
||||
// should trigger a reconnect
|
||||
expect(Scheduler).toHaveYielded(['Connected! theme: dark']);
|
||||
});
|
||||
|
||||
// @gate enableUseEventHook
|
||||
it('integration: implements the docs logVisit example', () => {
|
||||
class AddToCartButton extends React.PureComponent {
|
||||
addToCart = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
render() {
|
||||
return <Text text="Add to cart" />;
|
||||
}
|
||||
}
|
||||
const ShoppingCartContext = createContext(null);
|
||||
|
||||
function AppShell({children}) {
|
||||
const [items, updateItems] = useState([]);
|
||||
const value = useMemo(() => ({items, updateItems}), [items, updateItems]);
|
||||
|
||||
return (
|
||||
<ShoppingCartContext.Provider value={value}>
|
||||
{children}
|
||||
</ShoppingCartContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Page({url}) {
|
||||
const {items, updateItems} = useContext(ShoppingCartContext);
|
||||
const onClick = useEvent(() => updateItems([...items, 1]));
|
||||
const numberOfItems = items.length;
|
||||
|
||||
const onVisit = useEvent(visitedUrl => {
|
||||
Scheduler.unstable_yieldValue(
|
||||
'url: ' + url + ', numberOfItems: ' + numberOfItems,
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onVisit(url);
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
<AddToCartButton
|
||||
onClick={() => {
|
||||
onClick();
|
||||
}}
|
||||
ref={button}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const button = React.createRef(null);
|
||||
act(() =>
|
||||
ReactNoop.render(
|
||||
<AppShell>
|
||||
<Page url="/shop/1" />
|
||||
</AppShell>,
|
||||
),
|
||||
);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Add to cart',
|
||||
'url: /shop/1, numberOfItems: 0',
|
||||
]);
|
||||
act(button.current.addToCart);
|
||||
expect(Scheduler).toHaveYielded(['Add to cart']);
|
||||
|
||||
act(() =>
|
||||
ReactNoop.render(
|
||||
<AppShell>
|
||||
<Page url="/shop/2" />
|
||||
</AppShell>,
|
||||
),
|
||||
);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
'Add to cart',
|
||||
'url: /shop/2, numberOfItems: 1',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -51,6 +51,7 @@ export {
|
||||
useDeferredValue,
|
||||
useDeferredValue as unstable_useDeferredValue, // TODO: Remove once call sights updated to useDeferredValue
|
||||
useEffect,
|
||||
experimental_useEvent,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useInsertionEffect,
|
||||
|
||||
@@ -43,6 +43,7 @@ export {
|
||||
useDebugValue,
|
||||
useDeferredValue,
|
||||
useEffect,
|
||||
experimental_useEvent,
|
||||
useImperativeHandle,
|
||||
useInsertionEffect,
|
||||
useLayoutEffect,
|
||||
|
||||
@@ -71,6 +71,7 @@ export {
|
||||
useDebugValue,
|
||||
useDeferredValue,
|
||||
useEffect,
|
||||
experimental_useEvent,
|
||||
useImperativeHandle,
|
||||
useInsertionEffect,
|
||||
useLayoutEffect,
|
||||
|
||||
@@ -49,6 +49,7 @@ export {
|
||||
useDeferredValue,
|
||||
useDeferredValue as unstable_useDeferredValue, // TODO: Remove once call sights updated to useDeferredValue
|
||||
useEffect,
|
||||
experimental_useEvent,
|
||||
useImperativeHandle,
|
||||
useInsertionEffect,
|
||||
useLayoutEffect,
|
||||
|
||||
@@ -33,6 +33,7 @@ export {
|
||||
useDebugValue,
|
||||
useDeferredValue,
|
||||
useEffect,
|
||||
experimental_useEvent,
|
||||
useImperativeHandle,
|
||||
useInsertionEffect,
|
||||
useLayoutEffect,
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useEvent,
|
||||
useImperativeHandle,
|
||||
useDebugValue,
|
||||
useInsertionEffect,
|
||||
@@ -96,6 +97,7 @@ export {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useEvent as experimental_useEvent,
|
||||
useImperativeHandle,
|
||||
useDebugValue,
|
||||
useInsertionEffect,
|
||||
|
||||
@@ -217,3 +217,9 @@ export function useMemoCache(size: number): Array<any> {
|
||||
// $FlowFixMe This is unstable, thus optional
|
||||
return dispatcher.useMemoCache(size);
|
||||
}
|
||||
|
||||
export function useEvent<T>(callback: T): void {
|
||||
const dispatcher = resolveDispatcher();
|
||||
// $FlowFixMe This is unstable, thus optional
|
||||
return dispatcher.useEvent(callback);
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@ export const enableUseHook = __EXPERIMENTAL__;
|
||||
// auto-memoization.
|
||||
export const enableUseMemoCacheHook = __EXPERIMENTAL__;
|
||||
|
||||
export const enableUseEventHook = __EXPERIMENTAL__;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Chopping Block
|
||||
//
|
||||
|
||||
@@ -52,6 +52,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
|
||||
export const enableCPUSuspense = true;
|
||||
export const enableUseHook = false;
|
||||
export const enableUseMemoCacheHook = false;
|
||||
export const enableUseEventHook = false;
|
||||
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
|
||||
export const enableClientRenderFallbackOnTextMismatch = true;
|
||||
export const enableComponentStackLocations = false;
|
||||
|
||||
@@ -42,6 +42,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
|
||||
export const enableCPUSuspense = false;
|
||||
export const enableUseHook = false;
|
||||
export const enableUseMemoCacheHook = false;
|
||||
export const enableUseEventHook = false;
|
||||
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
|
||||
export const enableClientRenderFallbackOnTextMismatch = true;
|
||||
export const enableComponentStackLocations = false;
|
||||
|
||||
@@ -42,6 +42,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
|
||||
export const enableCPUSuspense = false;
|
||||
export const enableUseHook = false;
|
||||
export const enableUseMemoCacheHook = false;
|
||||
export const enableUseEventHook = false;
|
||||
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
|
||||
export const enableClientRenderFallbackOnTextMismatch = true;
|
||||
export const enableComponentStackLocations = true;
|
||||
|
||||
@@ -51,6 +51,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
|
||||
export const enableCPUSuspense = false;
|
||||
export const enableUseHook = false;
|
||||
export const enableUseMemoCacheHook = false;
|
||||
export const enableUseEventHook = false;
|
||||
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
|
||||
export const enableClientRenderFallbackOnTextMismatch = true;
|
||||
export const enableStrictEffects = false;
|
||||
|
||||
@@ -42,6 +42,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
|
||||
export const enableCPUSuspense = false;
|
||||
export const enableUseHook = false;
|
||||
export const enableUseMemoCacheHook = false;
|
||||
export const enableUseEventHook = false;
|
||||
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
|
||||
export const enableClientRenderFallbackOnTextMismatch = true;
|
||||
export const enableComponentStackLocations = true;
|
||||
|
||||
@@ -42,6 +42,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
|
||||
export const enableCPUSuspense = false;
|
||||
export const enableUseHook = false;
|
||||
export const enableUseMemoCacheHook = false;
|
||||
export const enableUseEventHook = false;
|
||||
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
|
||||
export const enableClientRenderFallbackOnTextMismatch = true;
|
||||
export const enableComponentStackLocations = true;
|
||||
|
||||
@@ -42,6 +42,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
|
||||
export const enableCPUSuspense = true;
|
||||
export const enableUseHook = false;
|
||||
export const enableUseMemoCacheHook = false;
|
||||
export const enableUseEventHook = false;
|
||||
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
|
||||
export const enableClientRenderFallbackOnTextMismatch = true;
|
||||
export const enableComponentStackLocations = true;
|
||||
|
||||
@@ -55,6 +55,7 @@ export const enableCPUSuspense = true;
|
||||
export const enableFloat = false;
|
||||
export const enableUseHook = true;
|
||||
export const enableUseMemoCacheHook = true;
|
||||
export const enableUseEventHook = true;
|
||||
|
||||
// Logs additional User Timing API marks for use with an experimental profiling tool.
|
||||
export const enableSchedulingProfiler: boolean =
|
||||
|
||||
@@ -424,5 +424,6 @@
|
||||
"436": "Stylesheet resources need a unique representation in the DOM while hydrating and more than one matching DOM Node was found. To fix, ensure you are only rendering one stylesheet link with an href attribute of \"%s\".",
|
||||
"437": "the \"precedence\" prop for links to stylesheets expects to receive a string but received something of type \"%s\" instead.",
|
||||
"438": "An unsupported type was passed to use(): %s",
|
||||
"439": "We didn't expect to see a forward reference. This is a bug in the React Server."
|
||||
"439": "We didn't expect to see a forward reference. This is a bug in the React Server.",
|
||||
"440": "An event from useEvent was called during render."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user