@samitouri / QOS-React-2 / commits / c91a1e03be

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 }