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 <rickhanlonii@gmail.com> Co-authored-by: Rick Hanlon <rickhanlonii@fb.com> Co-authored-by: Lauren Tan <poteto@users.noreply.github.com>
Lauren Tan committed
Sep 14, 2022 at 14:39 UTC
c91a1e03be54733a7dbfcb5663d7a9e8606ab1c1
26 files changed
+1027
-38
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+34
-16
@@ -48,6 +48,7 @@ import {
48
enableUpdaterTracking,
49
enableCache,
50
enableTransitionTracing,
51
+ enableUseEventHook,
52
} from 'shared/ReactFeatureFlags';
53
import {
54
FunctionComponent,
@@ -162,6 +163,7 @@ import {
163
Layout as HookLayout,
164
Insertion as HookInsertion,
165
Passive as HookPassive,
166
+ Snapshot as HookSnapshot,
167
} from './ReactHookEffectTags';
168
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new';
169
import {doesFiberContain} from './ReactFiberTreeReflection';
@@ -407,14 +409,24 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
409
410
if ((flags & Snapshot) !== NoFlags) {
411
setCurrentDebugFiberInDEV(finishedWork);
412
+ }
413
411
- switch (finishedWork.tag) {
412
- case FunctionComponent:
413
- case ForwardRef:
414
- case SimpleMemoComponent: {
415
- break;
414
+ switch (finishedWork.tag) {
415
+ case FunctionComponent: {
416
+ if (enableUseEventHook) {
417
+ if ((flags & Update) !== NoFlags) {
418
+ // useEvent doesn't need to be cleaned up
419
+ commitHookEffectListMount(HookSnapshot | HookHasEffect, finishedWork);
420
+ }
421
}
417
- case ClassComponent: {
422
+ break;
423
+ }
424
+ case ForwardRef:
425
+ case SimpleMemoComponent: {
426
+ break;
427
+ }
428
+ case ClassComponent: {
429
+ if ((flags & Snapshot) !== NoFlags) {
430
if (current !== null) {
431
const prevProps = current.memoizedProps;
432
const prevState = current.memoizedState;
@@ -468,29 +480,35 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
480
}
481
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
482
}
471
- break;
483
}
473
- case HostRoot: {
484
+ break;
485
+ }
486
+ case HostRoot: {
487
+ if ((flags & Snapshot) !== NoFlags) {
488
if (supportsMutation) {
489
const root = finishedWork.stateNode;
490
clearContainer(root.containerInfo);
491
}
478
- break;
492
}
480
- case HostComponent:
481
- case HostText:
482
- case HostPortal:
483
- case IncompleteClassComponent:
484
- // Nothing to do for these component types
485
- break;
486
- default: {
493
+ break;
494
+ }
495
+ case HostComponent:
496
+ case HostText:
497
+ case HostPortal:
498
+ case IncompleteClassComponent:
499
+ // Nothing to do for these component types
500
+ break;
501
+ default: {
502
+ if ((flags & Snapshot) !== NoFlags) {
503
throw new Error(
504
'This unit of work tag should not have side-effects. This error is ' +
505
'likely caused by a bug in React. Please file an issue.',
506
);
507
}
508
}
509
+ }
510
511
+ if ((flags & Snapshot) !== NoFlags) {
512
resetCurrentDebugFiberInDEV();
513
}
514
}
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+34
-16
@@ -48,6 +48,7 @@ import {
48
enableUpdaterTracking,
49
enableCache,
50
enableTransitionTracing,
51
+ enableUseEventHook,
52
} from 'shared/ReactFeatureFlags';
53
import {
54
FunctionComponent,
@@ -162,6 +163,7 @@ import {
163
Layout as HookLayout,
164
Insertion as HookInsertion,
165
Passive as HookPassive,
166
+ Snapshot as HookSnapshot,
167
} from './ReactHookEffectTags';
168
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old';
169
import {doesFiberContain} from './ReactFiberTreeReflection';
@@ -407,14 +409,24 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
409
410
if ((flags & Snapshot) !== NoFlags) {
411
setCurrentDebugFiberInDEV(finishedWork);
412
+ }
413
411
- switch (finishedWork.tag) {
412
- case FunctionComponent:
413
- case ForwardRef:
414
- case SimpleMemoComponent: {
415
- break;
414
+ switch (finishedWork.tag) {
415
+ case FunctionComponent: {
416
+ if (enableUseEventHook) {
417
+ if ((flags & Update) !== NoFlags) {
418
+ // useEvent doesn't need to be cleaned up
419
+ commitHookEffectListMount(HookSnapshot | HookHasEffect, finishedWork);
420
+ }
421
}
417
- case ClassComponent: {
422
+ break;
423
+ }
424
+ case ForwardRef:
425
+ case SimpleMemoComponent: {
426
+ break;
427
+ }
428
+ case ClassComponent: {
429
+ if ((flags & Snapshot) !== NoFlags) {
430
if (current !== null) {
431
const prevProps = current.memoizedProps;
432
const prevState = current.memoizedState;
@@ -468,29 +480,35 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
480
}
481
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
482
}
471
- break;
483
}
473
- case HostRoot: {
484
+ break;
485
+ }
486
+ case HostRoot: {
487
+ if ((flags & Snapshot) !== NoFlags) {
488
if (supportsMutation) {
489
const root = finishedWork.stateNode;
490
clearContainer(root.containerInfo);
491
}
478
- break;
492
}
480
- case HostComponent:
481
- case HostText:
482
- case HostPortal:
483
- case IncompleteClassComponent:
484
- // Nothing to do for these component types
485
- break;
486
- default: {
493
+ break;
494
+ }
495
+ case HostComponent:
496
+ case HostText:
497
+ case HostPortal:
498
+ case IncompleteClassComponent:
499
+ // Nothing to do for these component types
500
+ break;
501
+ default: {
502
+ if ((flags & Snapshot) !== NoFlags) {
503
throw new Error(
504
'This unit of work tag should not have side-effects. This error is ' +
505
'likely caused by a bug in React. Please file an issue.',
506
);
507
}
508
}
509
+ }
510
511
+ if ((flags & Snapshot) !== NoFlags) {
512
resetCurrentDebugFiberInDEV();
513
}
514
}
packages/react-reconciler/src/ReactFiberHooks.new.js
+124
@@ -40,6 +40,7 @@ import {
40
enableTransitionTracing,
41
enableUseHook,
42
enableUseMemoCacheHook,
43
+ enableUseEventHook,
44
} from 'shared/ReactFeatureFlags';
45
import {
46
REACT_CONTEXT_TYPE,
@@ -85,6 +86,7 @@ import {
86
Layout as HookLayout,
87
Passive as HookPassive,
88
Insertion as HookInsertion,
89
+ Snapshot as HookSnapshot,
90
} from './ReactHookEffectTags';
91
import {
92
getWorkInProgressRoot,
@@ -93,6 +95,7 @@ import {
95
requestUpdateLane,
96
requestEventTime,
97
markSkippedUpdateLanes,
98
+ isInvalidExecutionContextForEventFunction,
99
} from './ReactFiberWorkLoop.new';
100
101
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
@@ -1868,6 +1871,49 @@ function updateEffect(
1871
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
1872
}
1873
1874
+function mountEvent<T>(callback: () => T): () => T {
1875
+ const hook = mountWorkInProgressHook();
1876
+ const ref = {current: callback};
1877
+
1878
+ function event() {
1879
+ if (isInvalidExecutionContextForEventFunction()) {
1880
+ throw new Error('An event from useEvent was called during render.');
1881
+ }
1882
+ return ref.current.apply(undefined, arguments);
1883
+ }
1884
+
1885
+ // TODO: We don't need all the overhead of an effect object since there are no deps and no
1886
+ // clean up functions.
1887
+ mountEffectImpl(
1888
+ UpdateEffect,
1889
+ HookSnapshot,
1890
+ () => {
1891
+ ref.current = callback;
1892
+ },
1893
+ [ref, callback],
1894
+ );
1895
+
1896
+ hook.memoizedState = [ref, event];
1897
+
1898
+ return event;
1899
+}
1900
+
1901
+function updateEvent<T>(callback: () => T): () => T {
1902
+ const hook = updateWorkInProgressHook();
1903
+ const ref = hook.memoizedState[0];
1904
+
1905
+ updateEffectImpl(
1906
+ UpdateEffect,
1907
+ HookSnapshot,
1908
+ () => {
1909
+ ref.current = callback;
1910
+ },
1911
+ [ref, callback],
1912
+ );
1913
+
1914
+ return hook.memoizedState[1];
1915
+}
1916
+
1917
function mountInsertionEffect(
1918
create: () => (() => void) | void,
1919
deps: Array<mixed> | void | null,
@@ -2546,6 +2592,9 @@ if (enableUseHook) {
2592
if (enableUseMemoCacheHook) {
2593
(ContextOnlyDispatcher: Dispatcher).useMemoCache = throwInvalidHookError;
2594
}
2595
+if (enableUseEventHook) {
2596
+ (ContextOnlyDispatcher: Dispatcher).useEvent = throwInvalidHookError;
2597
+}
2598
2599
const HooksDispatcherOnMount: Dispatcher = {
2600
readContext,
@@ -2581,6 +2630,9 @@ if (enableUseHook) {
2630
if (enableUseMemoCacheHook) {
2631
(HooksDispatcherOnMount: Dispatcher).useMemoCache = useMemoCache;
2632
}
2633
+if (enableUseEventHook) {
2634
+ (HooksDispatcherOnMount: Dispatcher).useEvent = mountEvent;
2635
+}
2636
const HooksDispatcherOnUpdate: Dispatcher = {
2637
readContext,
2638
@@ -2614,6 +2666,9 @@ if (enableUseMemoCacheHook) {
2666
if (enableUseHook) {
2667
(HooksDispatcherOnUpdate: Dispatcher).use = use;
2668
}
2669
+if (enableUseEventHook) {
2670
+ (HooksDispatcherOnUpdate: Dispatcher).useEvent = updateEvent;
2671
+}
2672
2673
const HooksDispatcherOnRerender: Dispatcher = {
2674
readContext,
@@ -2648,6 +2703,9 @@ if (enableUseHook) {
2703
if (enableUseMemoCacheHook) {
2704
(HooksDispatcherOnRerender: Dispatcher).useMemoCache = useMemoCache;
2705
}
2706
+if (enableUseEventHook) {
2707
+ (HooksDispatcherOnRerender: Dispatcher).useEvent = updateEvent;
2708
+}
2709
2710
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
2711
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
@@ -2829,6 +2887,15 @@ if (__DEV__) {
2887
if (enableUseMemoCacheHook) {
2888
(HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache;
2889
}
2890
+ if (enableUseEventHook) {
2891
+ (HooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent<T>(
2892
+ callback: () => T,
2893
+ ): () => T {
2894
+ currentHookNameInDev = 'useEvent';
2895
+ mountHookTypesDev();
2896
+ return mountEvent(callback);
2897
+ };
2898
+ }
2899
2900
HooksDispatcherOnMountWithHookTypesInDEV = {
2901
readContext<T>(context: ReactContext<T>): T {
@@ -2977,6 +3044,15 @@ if (__DEV__) {
3044
if (enableUseMemoCacheHook) {
3045
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useMemoCache = useMemoCache;
3046
}
3047
+ if (enableUseEventHook) {
3048
+ (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useEvent = function useEvent<
3049
+ T,
3050
+ >(callback: () => T): () => T {
3051
+ currentHookNameInDev = 'useEvent';
3052
+ updateHookTypesDev();
3053
+ return mountEvent(callback);
3054
+ };
3055
+ }
3056
3057
HooksDispatcherOnUpdateInDEV = {
3058
readContext<T>(context: ReactContext<T>): T {
@@ -3125,6 +3201,15 @@ if (__DEV__) {
3201
if (enableUseMemoCacheHook) {
3202
(HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache;
3203
}
3204
+ if (enableUseEventHook) {
3205
+ (HooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent<T>(
3206
+ callback: () => T,
3207
+ ): () => T {
3208
+ currentHookNameInDev = 'useEvent';
3209
+ updateHookTypesDev();
3210
+ return updateEvent(callback);
3211
+ };
3212
+ }
3213
3214
HooksDispatcherOnRerenderInDEV = {
3215
readContext<T>(context: ReactContext<T>): T {
@@ -3274,6 +3359,15 @@ if (__DEV__) {
3359
if (enableUseMemoCacheHook) {
3360
(HooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = useMemoCache;
3361
}
3362
+ if (enableUseEventHook) {
3363
+ (HooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent<
3364
+ T,
3365
+ >(callback: () => T): () => T {
3366
+ currentHookNameInDev = 'useEvent';
3367
+ updateHookTypesDev();
3368
+ return updateEvent(callback);
3369
+ };
3370
+ }
3371
3372
InvalidNestedHooksDispatcherOnMountInDEV = {
3373
readContext<T>(context: ReactContext<T>): T {
@@ -3449,6 +3543,16 @@ if (__DEV__) {
3543
return useMemoCache(size);
3544
};
3545
}
3546
+ if (enableUseEventHook) {
3547
+ (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent<
3548
+ T,
3549
+ >(callback: () => T): () => T {
3550
+ currentHookNameInDev = 'useEvent';
3551
+ warnInvalidHookAccess();
3552
+ mountHookTypesDev();
3553
+ return mountEvent(callback);
3554
+ };
3555
+ }
3556
3557
InvalidNestedHooksDispatcherOnUpdateInDEV = {
3558
readContext<T>(context: ReactContext<T>): T {
@@ -3624,6 +3728,16 @@ if (__DEV__) {
3728
return useMemoCache(size);
3729
};
3730
}
3731
+ if (enableUseEventHook) {
3732
+ (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent<
3733
+ T,
3734
+ >(callback: () => T): () => T {
3735
+ currentHookNameInDev = 'useEvent';
3736
+ warnInvalidHookAccess();
3737
+ updateHookTypesDev();
3738
+ return updateEvent(callback);
3739
+ };
3740
+ }
3741
3742
InvalidNestedHooksDispatcherOnRerenderInDEV = {
3743
readContext<T>(context: ReactContext<T>): T {
@@ -3800,4 +3914,14 @@ if (__DEV__) {
3914
return useMemoCache(size);
3915
};
3916
}
3917
+ if (enableUseEventHook) {
3918
+ (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent<
3919
+ T,
3920
+ >(callback: () => T): () => T {
3921
+ currentHookNameInDev = 'useEvent';
3922
+ warnInvalidHookAccess();
3923
+ updateHookTypesDev();
3924
+ return updateEvent(callback);
3925
+ };
3926
+ }
3927
}
packages/react-reconciler/src/ReactFiberHooks.old.js
+124
@@ -40,6 +40,7 @@ import {
40
enableTransitionTracing,
41
enableUseHook,
42
enableUseMemoCacheHook,
43
+ enableUseEventHook,
44
} from 'shared/ReactFeatureFlags';
45
import {
46
REACT_CONTEXT_TYPE,
@@ -85,6 +86,7 @@ import {
86
Layout as HookLayout,
87
Passive as HookPassive,
88
Insertion as HookInsertion,
89
+ Snapshot as HookSnapshot,
90
} from './ReactHookEffectTags';
91
import {
92
getWorkInProgressRoot,
@@ -93,6 +95,7 @@ import {
95
requestUpdateLane,
96
requestEventTime,
97
markSkippedUpdateLanes,
98
+ isInvalidExecutionContextForEventFunction,
99
} from './ReactFiberWorkLoop.old';
100
101
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
@@ -1868,6 +1871,49 @@ function updateEffect(
1871
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
1872
}
1873
1874
+function mountEvent<T>(callback: () => T): () => T {
1875
+ const hook = mountWorkInProgressHook();
1876
+ const ref = {current: callback};
1877
+
1878
+ function event() {
1879
+ if (isInvalidExecutionContextForEventFunction()) {
1880
+ throw new Error('An event from useEvent was called during render.');
1881
+ }
1882
+ return ref.current.apply(undefined, arguments);
1883
+ }
1884
+
1885
+ // TODO: We don't need all the overhead of an effect object since there are no deps and no
1886
+ // clean up functions.
1887
+ mountEffectImpl(
1888
+ UpdateEffect,
1889
+ HookSnapshot,
1890
+ () => {
1891
+ ref.current = callback;
1892
+ },
1893
+ [ref, callback],
1894
+ );
1895
+
1896
+ hook.memoizedState = [ref, event];
1897
+
1898
+ return event;
1899
+}
1900
+
1901
+function updateEvent<T>(callback: () => T): () => T {
1902
+ const hook = updateWorkInProgressHook();
1903
+ const ref = hook.memoizedState[0];
1904
+
1905
+ updateEffectImpl(
1906
+ UpdateEffect,
1907
+ HookSnapshot,
1908
+ () => {
1909
+ ref.current = callback;
1910
+ },
1911
+ [ref, callback],
1912
+ );
1913
+
1914
+ return hook.memoizedState[1];
1915
+}
1916
+
1917
function mountInsertionEffect(
1918
create: () => (() => void) | void,
1919
deps: Array<mixed> | void | null,
@@ -2546,6 +2592,9 @@ if (enableUseHook) {
2592
if (enableUseMemoCacheHook) {
2593
(ContextOnlyDispatcher: Dispatcher).useMemoCache = throwInvalidHookError;
2594
}
2595
+if (enableUseEventHook) {
2596
+ (ContextOnlyDispatcher: Dispatcher).useEvent = throwInvalidHookError;
2597
+}
2598
2599
const HooksDispatcherOnMount: Dispatcher = {
2600
readContext,
@@ -2581,6 +2630,9 @@ if (enableUseHook) {
2630
if (enableUseMemoCacheHook) {
2631
(HooksDispatcherOnMount: Dispatcher).useMemoCache = useMemoCache;
2632
}
2633
+if (enableUseEventHook) {
2634
+ (HooksDispatcherOnMount: Dispatcher).useEvent = mountEvent;
2635
+}
2636
const HooksDispatcherOnUpdate: Dispatcher = {
2637
readContext,
2638
@@ -2614,6 +2666,9 @@ if (enableUseMemoCacheHook) {
2666
if (enableUseHook) {
2667
(HooksDispatcherOnUpdate: Dispatcher).use = use;
2668
}
2669
+if (enableUseEventHook) {
2670
+ (HooksDispatcherOnUpdate: Dispatcher).useEvent = updateEvent;
2671
+}
2672
2673
const HooksDispatcherOnRerender: Dispatcher = {
2674
readContext,
@@ -2648,6 +2703,9 @@ if (enableUseHook) {
2703
if (enableUseMemoCacheHook) {
2704
(HooksDispatcherOnRerender: Dispatcher).useMemoCache = useMemoCache;
2705
}
2706
+if (enableUseEventHook) {
2707
+ (HooksDispatcherOnRerender: Dispatcher).useEvent = updateEvent;
2708
+}
2709
2710
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
2711
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
@@ -2829,6 +2887,15 @@ if (__DEV__) {
2887
if (enableUseMemoCacheHook) {
2888
(HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache;
2889
}
2890
+ if (enableUseEventHook) {
2891
+ (HooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent<T>(
2892
+ callback: () => T,
2893
+ ): () => T {
2894
+ currentHookNameInDev = 'useEvent';
2895
+ mountHookTypesDev();
2896
+ return mountEvent(callback);
2897
+ };
2898
+ }
2899
2900
HooksDispatcherOnMountWithHookTypesInDEV = {
2901
readContext<T>(context: ReactContext<T>): T {
@@ -2977,6 +3044,15 @@ if (__DEV__) {
3044
if (enableUseMemoCacheHook) {
3045
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useMemoCache = useMemoCache;
3046
}
3047
+ if (enableUseEventHook) {
3048
+ (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useEvent = function useEvent<
3049
+ T,
3050
+ >(callback: () => T): () => T {
3051
+ currentHookNameInDev = 'useEvent';
3052
+ updateHookTypesDev();
3053
+ return mountEvent(callback);
3054
+ };
3055
+ }
3056
3057
HooksDispatcherOnUpdateInDEV = {
3058
readContext<T>(context: ReactContext<T>): T {
@@ -3125,6 +3201,15 @@ if (__DEV__) {
3201
if (enableUseMemoCacheHook) {
3202
(HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache;
3203
}
3204
+ if (enableUseEventHook) {
3205
+ (HooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent<T>(
3206
+ callback: () => T,
3207
+ ): () => T {
3208
+ currentHookNameInDev = 'useEvent';
3209
+ updateHookTypesDev();
3210
+ return updateEvent(callback);
3211
+ };
3212
+ }
3213
3214
HooksDispatcherOnRerenderInDEV = {
3215
readContext<T>(context: ReactContext<T>): T {
@@ -3274,6 +3359,15 @@ if (__DEV__) {
3359
if (enableUseMemoCacheHook) {
3360
(HooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = useMemoCache;
3361
}
3362
+ if (enableUseEventHook) {
3363
+ (HooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent<
3364
+ T,
3365
+ >(callback: () => T): () => T {
3366
+ currentHookNameInDev = 'useEvent';
3367
+ updateHookTypesDev();
3368
+ return updateEvent(callback);
3369
+ };
3370
+ }
3371
3372
InvalidNestedHooksDispatcherOnMountInDEV = {
3373
readContext<T>(context: ReactContext<T>): T {
@@ -3449,6 +3543,16 @@ if (__DEV__) {
3543
return useMemoCache(size);
3544
};
3545
}
3546
+ if (enableUseEventHook) {
3547
+ (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent<
3548
+ T,
3549
+ >(callback: () => T): () => T {
3550
+ currentHookNameInDev = 'useEvent';
3551
+ warnInvalidHookAccess();
3552
+ mountHookTypesDev();
3553
+ return mountEvent(callback);
3554
+ };
3555
+ }
3556
3557
InvalidNestedHooksDispatcherOnUpdateInDEV = {
3558
readContext<T>(context: ReactContext<T>): T {
@@ -3624,6 +3728,16 @@ if (__DEV__) {
3728
return useMemoCache(size);
3729
};
3730
}
3731
+ if (enableUseEventHook) {
3732
+ (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent<
3733
+ T,
3734
+ >(callback: () => T): () => T {
3735
+ currentHookNameInDev = 'useEvent';
3736
+ warnInvalidHookAccess();
3737
+ updateHookTypesDev();
3738
+ return updateEvent(callback);
3739
+ };
3740
+ }
3741
3742
InvalidNestedHooksDispatcherOnRerenderInDEV = {
3743
readContext<T>(context: ReactContext<T>): T {
@@ -3800,4 +3914,14 @@ if (__DEV__) {
3914
return useMemoCache(size);
3915
};
3916
}
3917
+ if (enableUseEventHook) {
3918
+ (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent<
3919
+ T,
3920
+ >(callback: () => T): () => T {
3921
+ currentHookNameInDev = 'useEvent';
3922
+ warnInvalidHookAccess();
3923
+ updateHookTypesDev();
3924
+ return updateEvent(callback);
3925
+ };
3926
+ }
3927
}
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+5
@@ -1605,6 +1605,11 @@ export function isAlreadyRendering(): boolean {
1605
);
1606
}
1607
1608
+export function isInvalidExecutionContextForEventFunction() {
1609
+ // Used to throw if certain APIs are called from the wrong context.
1610
+ return (executionContext & RenderContext) !== NoContext;
1611
+}
1612
+
1613
export function flushControlled(fn: () => mixed): void {
1614
const prevExecutionContext = executionContext;
1615
executionContext |= BatchedContext;
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+5
@@ -1605,6 +1605,11 @@ export function isAlreadyRendering(): boolean {
1605
);
1606
}
1607
1608
+export function isInvalidExecutionContextForEventFunction() {
1609
+ // Used to throw if certain APIs are called from the wrong context.
1610
+ return (executionContext & RenderContext) !== NoContext;
1611
+}
1612
+
1613
export function flushControlled(fn: () => mixed): void {
1614
const prevExecutionContext = executionContext;
1615
executionContext |= BatchedContext;
packages/react-reconciler/src/ReactHookEffectTags.js
+6
-5
@@ -9,12 +9,13 @@
9
10
export type HookFlags = number;
11
12
-export const NoFlags = /* */ 0b0000;
12
+export const NoFlags = /* */ 0b00000;
13
14
// Represents whether effect should fire.
15
-export const HasEffect = /* */ 0b0001;
15
+export const HasEffect = /* */ 0b00001;
16
17
// Represents the phase in which the effect (not the clean-up) fires.
18
-export const Insertion = /* */ 0b0010;
19
-export const Layout = /* */ 0b0100;
20
-export const Passive = /* */ 0b1000;
18
+export const Snapshot = /* */ 0b00010;
19
+export const Insertion = /* */ 0b00100;
20
+export const Layout = /* */ 0b01000;
21
+export const Passive = /* */ 0b10000;
packages/react-reconciler/src/ReactInternalTypes.js
+2
@@ -42,6 +42,7 @@ export type HookType =
42
| 'useContext'
43
| 'useRef'
44
| 'useEffect'
45
+ | 'useEvent'
46
| 'useInsertionEffect'
47
| 'useLayoutEffect'
48
| 'useCallback'
@@ -376,6 +377,7 @@ export type Dispatcher = {
377
create: () => (() => void) | void,
378
deps: Array<mixed> | void | null,
379
): void,
380
+ useEvent?: <T>(callback: () => T) => () => T,
381
useInsertionEffect(
382
create: () => (() => void) | void,
383
deps: Array<mixed> | void | null,
packages/react-reconciler/src/__tests__/useEvent-test.js
new
+668
@@ -0,0 +1,668 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ * @jest-environment node
9
+ */
10
+
11
+/* eslint-disable no-func-assign */
12
+
13
+'use strict';
14
+
15
+import {useInsertionEffect} from 'react';
16
+
17
+describe('useEvent', () => {
18
+ let React;
19
+ let ReactNoop;
20
+ let Scheduler;
21
+ let act;
22
+ let createContext;
23
+ let useContext;
24
+ let useState;
25
+ let useEvent;
26
+ let useEffect;
27
+ let useLayoutEffect;
28
+ let useMemo;
29
+
30
+ beforeEach(() => {
31
+ React = require('react');
32
+ ReactNoop = require('react-noop-renderer');
33
+ Scheduler = require('scheduler');
34
+
35
+ act = require('jest-react').act;
36
+ createContext = React.createContext;
37
+ useContext = React.useContext;
38
+ useState = React.useState;
39
+ useEvent = React.experimental_useEvent;
40
+ useEffect = React.useEffect;
41
+ useLayoutEffect = React.useLayoutEffect;
42
+ useMemo = React.useMemo;
43
+ });
44
+
45
+ function span(prop) {
46
+ return {type: 'span', hidden: false, children: [], prop};
47
+ }
48
+
49
+ function Text(props) {
50
+ Scheduler.unstable_yieldValue(props.text);
51
+ return <span prop={props.text} />;
52
+ }
53
+
54
+ // @gate enableUseEventHook
55
+ it('memoizes basic case correctly', () => {
56
+ class IncrementButton extends React.PureComponent {
57
+ increment = () => {
58
+ this.props.onClick();
59
+ };
60
+ render() {
61
+ return <Text text="Increment" />;
62
+ }
63
+ }
64
+
65
+ function Counter({incrementBy}) {
66
+ const [count, updateCount] = useState(0);
67
+ const onClick = useEvent(() => updateCount(c => c + incrementBy));
68
+
69
+ return (
70
+ <>
71
+ <IncrementButton onClick={() => onClick()} ref={button} />
72
+ <Text text={'Count: ' + count} />
73
+ </>
74
+ );
75
+ }
76
+
77
+ const button = React.createRef(null);
78
+ ReactNoop.render(<Counter incrementBy={1} />);
79
+ expect(Scheduler).toFlushAndYield(['Increment', 'Count: 0']);
80
+ expect(ReactNoop.getChildren()).toEqual([
81
+ span('Increment'),
82
+ span('Count: 0'),
83
+ ]);
84
+
85
+ act(button.current.increment);
86
+ expect(Scheduler).toHaveYielded(['Increment', 'Count: 1']);
87
+ expect(ReactNoop.getChildren()).toEqual([
88
+ span('Increment'),
89
+ span('Count: 1'),
90
+ ]);
91
+
92
+ act(button.current.increment);
93
+ expect(Scheduler).toHaveYielded([
94
+ 'Increment',
95
+ // Event should use the updated callback function closed over the new value.
96
+ 'Count: 2',
97
+ ]);
98
+ expect(ReactNoop.getChildren()).toEqual([
99
+ span('Increment'),
100
+ span('Count: 2'),
101
+ ]);
102
+
103
+ // Increase the increment prop amount
104
+ ReactNoop.render(<Counter incrementBy={10} />);
105
+ expect(Scheduler).toFlushAndYield(['Increment', 'Count: 2']);
106
+ expect(ReactNoop.getChildren()).toEqual([
107
+ span('Increment'),
108
+ span('Count: 2'),
109
+ ]);
110
+
111
+ // Event uses the new prop
112
+ act(button.current.increment);
113
+ expect(Scheduler).toHaveYielded(['Increment', 'Count: 12']);
114
+ expect(ReactNoop.getChildren()).toEqual([
115
+ span('Increment'),
116
+ span('Count: 12'),
117
+ ]);
118
+ });
119
+
120
+ // @gate enableUseEventHook
121
+ it('does not preserve `this` in event functions', () => {
122
+ class GreetButton extends React.PureComponent {
123
+ greet = () => {
124
+ this.props.onClick();
125
+ };
126
+ render() {
127
+ return <Text text={'Say ' + this.props.hello} />;
128
+ }
129
+ }
130
+ function Greeter({hello}) {
131
+ const person = {
132
+ toString() {
133
+ return 'Jane';
134
+ },
135
+ greet() {
136
+ return updateGreeting(this + ' says ' + hello);
137
+ },
138
+ };
139
+ const [greeting, updateGreeting] = useState('Seb says ' + hello);
140
+ const onClick = useEvent(person.greet);
141
+
142
+ return (
143
+ <>
144
+ <GreetButton hello={hello} onClick={() => onClick()} ref={button} />
145
+ <Text text={'Greeting: ' + greeting} />
146
+ </>
147
+ );
148
+ }
149
+
150
+ const button = React.createRef(null);
151
+ ReactNoop.render(<Greeter hello={'hej'} />);
152
+ expect(Scheduler).toFlushAndYield(['Say hej', 'Greeting: Seb says hej']);
153
+ expect(ReactNoop.getChildren()).toEqual([
154
+ span('Say hej'),
155
+ span('Greeting: Seb says hej'),
156
+ ]);
157
+
158
+ act(button.current.greet);
159
+ expect(Scheduler).toHaveYielded([
160
+ 'Say hej',
161
+ 'Greeting: undefined says hej',
162
+ ]);
163
+ expect(ReactNoop.getChildren()).toEqual([
164
+ span('Say hej'),
165
+ span('Greeting: undefined says hej'),
166
+ ]);
167
+ });
168
+
169
+ // @gate enableUseEventHook
170
+ it('throws when called in render', () => {
171
+ class IncrementButton extends React.PureComponent {
172
+ increment = () => {
173
+ this.props.onClick();
174
+ };
175
+
176
+ render() {
177
+ // Will throw.
178
+ this.props.onClick();
179
+
180
+ return <Text text="Increment" />;
181
+ }
182
+ }
183
+
184
+ function Counter({incrementBy}) {
185
+ const [count, updateCount] = useState(0);
186
+ const onClick = useEvent(() => updateCount(c => c + incrementBy));
187
+
188
+ return (
189
+ <>
190
+ <IncrementButton onClick={() => onClick()} />
191
+ <Text text={'Count: ' + count} />
192
+ </>
193
+ );
194
+ }
195
+
196
+ ReactNoop.render(<Counter incrementBy={1} />);
197
+ expect(Scheduler).toFlushAndThrow(
198
+ 'An event from useEvent was called during render',
199
+ );
200
+
201
+ // If something throws, we try one more time synchronously in case the error was
202
+ // caused by a data race. See recoverFromConcurrentError
203
+ expect(Scheduler).toHaveYielded(['Count: 0', 'Count: 0']);
204
+ });
205
+
206
+ // @gate enableUseEventHook
207
+ it("useLayoutEffect shouldn't re-fire when event handlers change", () => {
208
+ class IncrementButton extends React.PureComponent {
209
+ increment = () => {
210
+ this.props.onClick();
211
+ };
212
+ render() {
213
+ return <Text text="Increment" />;
214
+ }
215
+ }
216
+
217
+ function Counter({incrementBy}) {
218
+ const [count, updateCount] = useState(0);
219
+ const increment = useEvent(amount =>
220
+ updateCount(c => c + (amount || incrementBy)),
221
+ );
222
+
223
+ useLayoutEffect(() => {
224
+ Scheduler.unstable_yieldValue('Effect: by ' + incrementBy * 2);
225
+ increment(incrementBy * 2);
226
+ }, [incrementBy]);
227
+
228
+ return (
229
+ <>
230
+ <IncrementButton onClick={() => increment()} ref={button} />
231
+ <Text text={'Count: ' + count} />
232
+ </>
233
+ );
234
+ }
235
+
236
+ const button = React.createRef(null);
237
+ ReactNoop.render(<Counter incrementBy={1} />);
238
+ expect(Scheduler).toHaveYielded([]);
239
+ expect(Scheduler).toFlushAndYield([
240
+ 'Increment',
241
+ 'Count: 0',
242
+ 'Effect: by 2',
243
+ 'Increment',
244
+ 'Count: 2',
245
+ ]);
246
+ expect(ReactNoop.getChildren()).toEqual([
247
+ span('Increment'),
248
+ span('Count: 2'),
249
+ ]);
250
+
251
+ act(button.current.increment);
252
+ expect(Scheduler).toHaveYielded([
253
+ 'Increment',
254
+ // Effect should not re-run because the dependency hasn't changed.
255
+ 'Count: 3',
256
+ ]);
257
+ expect(ReactNoop.getChildren()).toEqual([
258
+ span('Increment'),
259
+ span('Count: 3'),
260
+ ]);
261
+
262
+ act(button.current.increment);
263
+ expect(Scheduler).toHaveYielded([
264
+ 'Increment',
265
+ // Event should use the updated callback function closed over the new value.
266
+ 'Count: 4',
267
+ ]);
268
+ expect(ReactNoop.getChildren()).toEqual([
269
+ span('Increment'),
270
+ span('Count: 4'),
271
+ ]);
272
+
273
+ // Increase the increment prop amount
274
+ ReactNoop.render(<Counter incrementBy={10} />);
275
+ expect(Scheduler).toFlushAndYield([
276
+ 'Increment',
277
+ 'Count: 4',
278
+ 'Effect: by 20',
279
+ 'Increment',
280
+ 'Count: 24',
281
+ ]);
282
+ expect(ReactNoop.getChildren()).toEqual([
283
+ span('Increment'),
284
+ span('Count: 24'),
285
+ ]);
286
+
287
+ // Event uses the new prop
288
+ act(button.current.increment);
289
+ expect(Scheduler).toHaveYielded(['Increment', 'Count: 34']);
290
+ expect(ReactNoop.getChildren()).toEqual([
291
+ span('Increment'),
292
+ span('Count: 34'),
293
+ ]);
294
+ });
295
+
296
+ // @gate enableUseEventHook
297
+ it("useEffect shouldn't re-fire when event handlers change", () => {
298
+ class IncrementButton extends React.PureComponent {
299
+ increment = () => {
300
+ this.props.onClick();
301
+ };
302
+ render() {
303
+ return <Text text="Increment" />;
304
+ }
305
+ }
306
+
307
+ function Counter({incrementBy}) {
308
+ const [count, updateCount] = useState(0);
309
+ const increment = useEvent(amount =>
310
+ updateCount(c => c + (amount || incrementBy)),
311
+ );
312
+
313
+ useEffect(() => {
314
+ Scheduler.unstable_yieldValue('Effect: by ' + incrementBy * 2);
315
+ increment(incrementBy * 2);
316
+ }, [incrementBy]);
317
+
318
+ return (
319
+ <>
320
+ <IncrementButton onClick={() => increment()} ref={button} />
321
+ <Text text={'Count: ' + count} />
322
+ </>
323
+ );
324
+ }
325
+
326
+ const button = React.createRef(null);
327
+ ReactNoop.render(<Counter incrementBy={1} />);
328
+ expect(Scheduler).toFlushAndYield([
329
+ 'Increment',
330
+ 'Count: 0',
331
+ 'Effect: by 2',
332
+ 'Increment',
333
+ 'Count: 2',
334
+ ]);
335
+ expect(ReactNoop.getChildren()).toEqual([
336
+ span('Increment'),
337
+ span('Count: 2'),
338
+ ]);
339
+
340
+ act(button.current.increment);
341
+ expect(Scheduler).toHaveYielded([
342
+ 'Increment',
343
+ // Effect should not re-run because the dependency hasn't changed.
344
+ 'Count: 3',
345
+ ]);
346
+ expect(ReactNoop.getChildren()).toEqual([
347
+ span('Increment'),
348
+ span('Count: 3'),
349
+ ]);
350
+
351
+ act(button.current.increment);
352
+ expect(Scheduler).toHaveYielded([
353
+ 'Increment',
354
+ // Event should use the updated callback function closed over the new value.
355
+ 'Count: 4',
356
+ ]);
357
+ expect(ReactNoop.getChildren()).toEqual([
358
+ span('Increment'),
359
+ span('Count: 4'),
360
+ ]);
361
+
362
+ // Increase the increment prop amount
363
+ ReactNoop.render(<Counter incrementBy={10} />);
364
+ expect(Scheduler).toFlushAndYield([
365
+ 'Increment',
366
+ 'Count: 4',
367
+ 'Effect: by 20',
368
+ 'Increment',
369
+ 'Count: 24',
370
+ ]);
371
+ expect(ReactNoop.getChildren()).toEqual([
372
+ span('Increment'),
373
+ span('Count: 24'),
374
+ ]);
375
+
376
+ // Event uses the new prop
377
+ act(button.current.increment);
378
+ expect(Scheduler).toHaveYielded(['Increment', 'Count: 34']);
379
+ expect(ReactNoop.getChildren()).toEqual([
380
+ span('Increment'),
381
+ span('Count: 34'),
382
+ ]);
383
+ });
384
+
385
+ // @gate enableUseEventHook
386
+ it('is stable in a custom hook', () => {
387
+ class IncrementButton extends React.PureComponent {
388
+ increment = () => {
389
+ this.props.onClick();
390
+ };
391
+ render() {
392
+ return <Text text="Increment" />;
393
+ }
394
+ }
395
+
396
+ function useCount(incrementBy) {
397
+ const [count, updateCount] = useState(0);
398
+ const increment = useEvent(amount =>
399
+ updateCount(c => c + (amount || incrementBy)),
400
+ );
401
+
402
+ return [count, increment];
403
+ }
404
+
405
+ function Counter({incrementBy}) {
406
+ const [count, increment] = useCount(incrementBy);
407
+
408
+ useEffect(() => {
409
+ Scheduler.unstable_yieldValue('Effect: by ' + incrementBy * 2);
410
+ increment(incrementBy * 2);
411
+ }, [incrementBy]);
412
+
413
+ return (
414
+ <>
415
+ <IncrementButton onClick={() => increment()} ref={button} />
416
+ <Text text={'Count: ' + count} />
417
+ </>
418
+ );
419
+ }
420
+
421
+ const button = React.createRef(null);
422
+ ReactNoop.render(<Counter incrementBy={1} />);
423
+ expect(Scheduler).toFlushAndYield([
424
+ 'Increment',
425
+ 'Count: 0',
426
+ 'Effect: by 2',
427
+ 'Increment',
428
+ 'Count: 2',
429
+ ]);
430
+ expect(ReactNoop.getChildren()).toEqual([
431
+ span('Increment'),
432
+ span('Count: 2'),
433
+ ]);
434
+
435
+ act(button.current.increment);
436
+ expect(Scheduler).toHaveYielded([
437
+ 'Increment',
438
+ // Effect should not re-run because the dependency hasn't changed.
439
+ 'Count: 3',
440
+ ]);
441
+ expect(ReactNoop.getChildren()).toEqual([
442
+ span('Increment'),
443
+ span('Count: 3'),
444
+ ]);
445
+
446
+ act(button.current.increment);
447
+ expect(Scheduler).toHaveYielded([
448
+ 'Increment',
449
+ // Event should use the updated callback function closed over the new value.
450
+ 'Count: 4',
451
+ ]);
452
+ expect(ReactNoop.getChildren()).toEqual([
453
+ span('Increment'),
454
+ span('Count: 4'),
455
+ ]);
456
+
457
+ // Increase the increment prop amount
458
+ ReactNoop.render(<Counter incrementBy={10} />);
459
+ expect(Scheduler).toFlushAndYield([
460
+ 'Increment',
461
+ 'Count: 4',
462
+ 'Effect: by 20',
463
+ 'Increment',
464
+ 'Count: 24',
465
+ ]);
466
+ expect(ReactNoop.getChildren()).toEqual([
467
+ span('Increment'),
468
+ span('Count: 24'),
469
+ ]);
470
+
471
+ // Event uses the new prop
472
+ act(button.current.increment);
473
+ expect(Scheduler).toHaveYielded(['Increment', 'Count: 34']);
474
+ expect(ReactNoop.getChildren()).toEqual([
475
+ span('Increment'),
476
+ span('Count: 34'),
477
+ ]);
478
+ });
479
+
480
+ // @gate enableUseEventHook
481
+ it('is mutated before all other effects', () => {
482
+ function Counter({value}) {
483
+ useInsertionEffect(() => {
484
+ Scheduler.unstable_yieldValue('Effect value: ' + value);
485
+ increment();
486
+ }, [value]);
487
+
488
+ // This is defined after the insertion effect, but it should
489
+ // update the event fn _before_ the insertion effect fires.
490
+ const increment = useEvent(() => {
491
+ Scheduler.unstable_yieldValue('Event value: ' + value);
492
+ });
493
+
494
+ return <></>;
495
+ }
496
+
497
+ ReactNoop.render(<Counter value={1} />);
498
+ expect(Scheduler).toFlushAndYield(['Effect value: 1', 'Event value: 1']);
499
+
500
+ act(() => ReactNoop.render(<Counter value={2} />));
501
+ expect(Scheduler).toHaveYielded(['Effect value: 2', 'Event value: 2']);
502
+ });
503
+
504
+ // @gate enableUseEventHook
505
+ it('integration: implements docs chat room example', () => {
506
+ function createConnection() {
507
+ let connectedCallback;
508
+ let timeout;
509
+ return {
510
+ connect() {
511
+ timeout = setTimeout(() => {
512
+ if (connectedCallback) {
513
+ connectedCallback();
514
+ }
515
+ }, 100);
516
+ },
517
+ on(event, callback) {
518
+ if (connectedCallback) {
519
+ throw Error('Cannot add the handler twice.');
520
+ }
521
+ if (event !== 'connected') {
522
+ throw Error('Only "connected" event is supported.');
523
+ }
524
+ connectedCallback = callback;
525
+ },
526
+ disconnect() {
527
+ clearTimeout(timeout);
528
+ },
529
+ };
530
+ }
531
+
532
+ function ChatRoom({roomId, theme}) {
533
+ const onConnected = useEvent(() => {
534
+ Scheduler.unstable_yieldValue('Connected! theme: ' + theme);
535
+ });
536
+
537
+ useEffect(() => {
538
+ const connection = createConnection(roomId);
539
+ connection.on('connected', () => {
540
+ onConnected();
541
+ });
542
+ connection.connect();
543
+ return () => connection.disconnect();
544
+ }, [roomId, onConnected]);
545
+
546
+ return <Text text={`Welcome to the ${roomId} room!`} />;
547
+ }
548
+
549
+ act(() => ReactNoop.render(<ChatRoom roomId="general" theme="light" />));
550
+ expect(Scheduler).toHaveYielded(['Welcome to the general room!']);
551
+ expect(ReactNoop.getChildren()).toEqual([
552
+ span('Welcome to the general room!'),
553
+ ]);
554
+
555
+ jest.advanceTimersByTime(100);
556
+ Scheduler.unstable_advanceTime(100);
557
+ expect(Scheduler).toHaveYielded(['Connected! theme: light']);
558
+
559
+ // change roomId only
560
+ act(() => ReactNoop.render(<ChatRoom roomId="music" theme="light" />));
561
+ expect(Scheduler).toHaveYielded(['Welcome to the music room!']);
562
+ expect(ReactNoop.getChildren()).toEqual([
563
+ span('Welcome to the music room!'),
564
+ ]);
565
+ jest.advanceTimersByTime(100);
566
+ Scheduler.unstable_advanceTime(100);
567
+ // should trigger a reconnect
568
+ expect(Scheduler).toHaveYielded(['Connected! theme: light']);
569
+
570
+ // change theme only
571
+ act(() => ReactNoop.render(<ChatRoom roomId="music" theme="dark" />));
572
+ expect(Scheduler).toHaveYielded(['Welcome to the music room!']);
573
+ expect(ReactNoop.getChildren()).toEqual([
574
+ span('Welcome to the music room!'),
575
+ ]);
576
+ jest.advanceTimersByTime(100);
577
+ Scheduler.unstable_advanceTime(100);
578
+ // should not trigger a reconnect
579
+ expect(Scheduler).toFlushWithoutYielding();
580
+
581
+ // change roomId only
582
+ act(() => ReactNoop.render(<ChatRoom roomId="travel" theme="dark" />));
583
+ expect(Scheduler).toHaveYielded(['Welcome to the travel room!']);
584
+ expect(ReactNoop.getChildren()).toEqual([
585
+ span('Welcome to the travel room!'),
586
+ ]);
587
+ jest.advanceTimersByTime(100);
588
+ Scheduler.unstable_advanceTime(100);
589
+ // should trigger a reconnect
590
+ expect(Scheduler).toHaveYielded(['Connected! theme: dark']);
591
+ });
592
+
593
+ // @gate enableUseEventHook
594
+ it('integration: implements the docs logVisit example', () => {
595
+ class AddToCartButton extends React.PureComponent {
596
+ addToCart = () => {
597
+ this.props.onClick();
598
+ };
599
+ render() {
600
+ return <Text text="Add to cart" />;
601
+ }
602
+ }
603
+ const ShoppingCartContext = createContext(null);
604
+
605
+ function AppShell({children}) {
606
+ const [items, updateItems] = useState([]);
607
+ const value = useMemo(() => ({items, updateItems}), [items, updateItems]);
608
+
609
+ return (
610
+ <ShoppingCartContext.Provider value={value}>
611
+ {children}
612
+ </ShoppingCartContext.Provider>
613
+ );
614
+ }
615
+
616
+ function Page({url}) {
617
+ const {items, updateItems} = useContext(ShoppingCartContext);
618
+ const onClick = useEvent(() => updateItems([...items, 1]));
619
+ const numberOfItems = items.length;
620
+
621
+ const onVisit = useEvent(visitedUrl => {
622
+ Scheduler.unstable_yieldValue(
623
+ 'url: ' + url + ', numberOfItems: ' + numberOfItems,
624
+ );
625
+ });
626
+
627
+ useEffect(() => {
628
+ onVisit(url);
629
+ }, [url]);
630
+
631
+ return (
632
+ <AddToCartButton
633
+ onClick={() => {
634
+ onClick();
635
+ }}
636
+ ref={button}
637
+ />
638
+ );
639
+ }
640
+
641
+ const button = React.createRef(null);
642
+ act(() =>
643
+ ReactNoop.render(
644
+ <AppShell>
645
+ <Page url="/shop/1" />
646
+ </AppShell>,
647
+ ),
648
+ );
649
+ expect(Scheduler).toHaveYielded([
650
+ 'Add to cart',
651
+ 'url: /shop/1, numberOfItems: 0',
652
+ ]);
653
+ act(button.current.addToCart);
654
+ expect(Scheduler).toHaveYielded(['Add to cart']);
655
+
656
+ act(() =>
657
+ ReactNoop.render(
658
+ <AppShell>
659
+ <Page url="/shop/2" />
660
+ </AppShell>,
661
+ ),
662
+ );
663
+ expect(Scheduler).toHaveYielded([
664
+ 'Add to cart',
665
+ 'url: /shop/2, numberOfItems: 1',
666
+ ]);
667
+ });
668
+});
packages/react/index.classic.fb.js
+1
@@ -51,6 +51,7 @@ export {
51
useDeferredValue,
52
useDeferredValue as unstable_useDeferredValue, // TODO: Remove once call sights updated to useDeferredValue
53
useEffect,
54
+ experimental_useEvent,
55
useImperativeHandle,
56
useLayoutEffect,
57
useInsertionEffect,
packages/react/index.experimental.js
+1
@@ -43,6 +43,7 @@ export {
43
useDebugValue,
44
useDeferredValue,
45
useEffect,
46
+ experimental_useEvent,
47
useImperativeHandle,
48
useInsertionEffect,
49
useLayoutEffect,
packages/react/index.js
+1
@@ -71,6 +71,7 @@ export {
71
useDebugValue,
72
useDeferredValue,
73
useEffect,
74
+ experimental_useEvent,
75
useImperativeHandle,
76
useInsertionEffect,
77
useLayoutEffect,
packages/react/index.modern.fb.js
+1
@@ -49,6 +49,7 @@ export {
49
useDeferredValue,
50
useDeferredValue as unstable_useDeferredValue, // TODO: Remove once call sights updated to useDeferredValue
51
useEffect,
52
+ experimental_useEvent,
53
useImperativeHandle,
54
useInsertionEffect,
55
useLayoutEffect,
packages/react/index.stable.js
+1
@@ -33,6 +33,7 @@ export {
33
useDebugValue,
34
useDeferredValue,
35
useEffect,
36
+ experimental_useEvent,
37
useImperativeHandle,
38
useInsertionEffect,
39
useLayoutEffect,
packages/react/src/React.js
+2
@@ -41,6 +41,7 @@ import {
41
useCallback,
42
useContext,
43
useEffect,
44
+ useEvent,
45
useImperativeHandle,
46
useDebugValue,
47
useInsertionEffect,
@@ -96,6 +97,7 @@ export {
97
useCallback,
98
useContext,
99
useEffect,
100
+ useEvent as experimental_useEvent,
101
useImperativeHandle,
102
useDebugValue,
103
useInsertionEffect,
packages/react/src/ReactHooks.js
+6
@@ -217,3 +217,9 @@ export function useMemoCache(size: number): Array<any> {
217
// $FlowFixMe This is unstable, thus optional
218
return dispatcher.useMemoCache(size);
219
}
220
+
221
+export function useEvent<T>(callback: T): void {
222
+ const dispatcher = resolveDispatcher();
223
+ // $FlowFixMe This is unstable, thus optional
224
+ return dispatcher.useEvent(callback);
225
+}
packages/shared/ReactFeatureFlags.js
+2
@@ -120,6 +120,8 @@ export const enableUseHook = __EXPERIMENTAL__;
120
// auto-memoization.
121
export const enableUseMemoCacheHook = __EXPERIMENTAL__;
122
123
+export const enableUseEventHook = __EXPERIMENTAL__;
124
+
125
// -----------------------------------------------------------------------------
126
// Chopping Block
127
//
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -52,6 +52,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
52
export const enableCPUSuspense = true;
53
export const enableUseHook = false;
54
export const enableUseMemoCacheHook = false;
55
+export const enableUseEventHook = false;
56
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
57
export const enableClientRenderFallbackOnTextMismatch = true;
58
export const enableComponentStackLocations = false;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -42,6 +42,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
42
export const enableCPUSuspense = false;
43
export const enableUseHook = false;
44
export const enableUseMemoCacheHook = false;
45
+export const enableUseEventHook = false;
46
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
47
export const enableClientRenderFallbackOnTextMismatch = true;
48
export const enableComponentStackLocations = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -42,6 +42,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
42
export const enableCPUSuspense = false;
43
export const enableUseHook = false;
44
export const enableUseMemoCacheHook = false;
45
+export const enableUseEventHook = false;
46
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
47
export const enableClientRenderFallbackOnTextMismatch = true;
48
export const enableComponentStackLocations = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -51,6 +51,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
51
export const enableCPUSuspense = false;
52
export const enableUseHook = false;
53
export const enableUseMemoCacheHook = false;
54
+export const enableUseEventHook = false;
55
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
56
export const enableClientRenderFallbackOnTextMismatch = true;
57
export const enableStrictEffects = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -42,6 +42,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
42
export const enableCPUSuspense = false;
43
export const enableUseHook = false;
44
export const enableUseMemoCacheHook = false;
45
+export const enableUseEventHook = false;
46
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
47
export const enableClientRenderFallbackOnTextMismatch = true;
48
export const enableComponentStackLocations = true;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -42,6 +42,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
42
export const enableCPUSuspense = false;
43
export const enableUseHook = false;
44
export const enableUseMemoCacheHook = false;
45
+export const enableUseEventHook = false;
46
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
47
export const enableClientRenderFallbackOnTextMismatch = true;
48
export const enableComponentStackLocations = true;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -42,6 +42,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
42
export const enableCPUSuspense = true;
43
export const enableUseHook = false;
44
export const enableUseMemoCacheHook = false;
45
+export const enableUseEventHook = false;
46
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true;
47
export const enableClientRenderFallbackOnTextMismatch = true;
48
export const enableComponentStackLocations = true;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -55,6 +55,7 @@ export const enableCPUSuspense = true;
55
export const enableFloat = false;
56
export const enableUseHook = true;
57
export const enableUseMemoCacheHook = true;
58
+export const enableUseEventHook = true;
59
60
// Logs additional User Timing API marks for use with an experimental profiling tool.
61
export const enableSchedulingProfiler: boolean =
scripts/error-codes/codes.json
+2
-1
@@ -424,5 +424,6 @@
424
"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\".",
425
"437": "the \"precedence\" prop for links to stylesheets expects to receive a string but received something of type \"%s\" instead.",
426
"438": "An unsupported type was passed to use(): %s",
427
- "439": "We didn't expect to see a forward reference. This is a bug in the React Server."
427
+ "439": "We didn't expect to see a forward reference. This is a bug in the React Server.",
428
+ "440": "An event from useEvent was called during render."
429
}