From c91a1e03be54733a7dbfcb5663d7a9e8606ab1c1 Mon Sep 17 00:00:00 2001 From: Lauren Tan Date: Wed, 14 Sep 2022 14:39:06 -0400 Subject: [PATCH] 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 Co-authored-by: Rick Hanlon Co-authored-by: Lauren Tan --- .../src/ReactFiberCommitWork.new.js | 50 +- .../src/ReactFiberCommitWork.old.js | 50 +- .../src/ReactFiberHooks.new.js | 124 ++++ .../src/ReactFiberHooks.old.js | 124 ++++ .../src/ReactFiberWorkLoop.new.js | 5 + .../src/ReactFiberWorkLoop.old.js | 5 + .../src/ReactHookEffectTags.js | 11 +- .../src/ReactInternalTypes.js | 2 + .../src/__tests__/useEvent-test.js | 668 ++++++++++++++++++ packages/react/index.classic.fb.js | 1 + packages/react/index.experimental.js | 1 + packages/react/index.js | 1 + packages/react/index.modern.fb.js | 1 + packages/react/index.stable.js | 1 + packages/react/src/React.js | 2 + packages/react/src/ReactHooks.js | 6 + packages/shared/ReactFeatureFlags.js | 2 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.native.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.testing.js | 1 + .../forks/ReactFeatureFlags.testing.www.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 1 + scripts/error-codes/codes.json | 3 +- 26 files changed, 1027 insertions(+), 38 deletions(-) create mode 100644 packages/react-reconciler/src/__tests__/useEvent-test.js diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 1710bcfc861e7..825652939f5e4 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -48,6 +48,7 @@ import { enableUpdaterTracking, enableCache, enableTransitionTracing, + enableUseEventHook, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -162,6 +163,7 @@ import { Layout as HookLayout, Insertion as HookInsertion, Passive as HookPassive, + Snapshot as HookSnapshot, } from './ReactHookEffectTags'; import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new'; import {doesFiberContain} from './ReactFiberTreeReflection'; @@ -407,14 +409,24 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) { if ((flags & Snapshot) !== NoFlags) { setCurrentDebugFiberInDEV(finishedWork); + } - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - break; + switch (finishedWork.tag) { + case FunctionComponent: { + if (enableUseEventHook) { + if ((flags & Update) !== NoFlags) { + // useEvent doesn't need to be cleaned up + commitHookEffectListMount(HookSnapshot | HookHasEffect, finishedWork); + } } - case ClassComponent: { + break; + } + case ForwardRef: + case SimpleMemoComponent: { + break; + } + case ClassComponent: { + if ((flags & Snapshot) !== NoFlags) { if (current !== null) { const prevProps = current.memoizedProps; const prevState = current.memoizedState; @@ -468,29 +480,35 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) { } instance.__reactInternalSnapshotBeforeUpdate = snapshot; } - break; } - case HostRoot: { + break; + } + case HostRoot: { + if ((flags & Snapshot) !== NoFlags) { if (supportsMutation) { const root = finishedWork.stateNode; clearContainer(root.containerInfo); } - break; } - case HostComponent: - case HostText: - case HostPortal: - case IncompleteClassComponent: - // Nothing to do for these component types - break; - default: { + break; + } + case HostComponent: + case HostText: + case HostPortal: + case IncompleteClassComponent: + // Nothing to do for these component types + break; + default: { + if ((flags & Snapshot) !== NoFlags) { throw new Error( 'This unit of work tag should not have side-effects. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); } } + } + if ((flags & Snapshot) !== NoFlags) { resetCurrentDebugFiberInDEV(); } } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index a7a8929d2b68c..57233404d6ff9 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -48,6 +48,7 @@ import { enableUpdaterTracking, enableCache, enableTransitionTracing, + enableUseEventHook, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -162,6 +163,7 @@ import { Layout as HookLayout, Insertion as HookInsertion, Passive as HookPassive, + Snapshot as HookSnapshot, } from './ReactHookEffectTags'; import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old'; import {doesFiberContain} from './ReactFiberTreeReflection'; @@ -407,14 +409,24 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) { if ((flags & Snapshot) !== NoFlags) { setCurrentDebugFiberInDEV(finishedWork); + } - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: { - break; + switch (finishedWork.tag) { + case FunctionComponent: { + if (enableUseEventHook) { + if ((flags & Update) !== NoFlags) { + // useEvent doesn't need to be cleaned up + commitHookEffectListMount(HookSnapshot | HookHasEffect, finishedWork); + } } - case ClassComponent: { + break; + } + case ForwardRef: + case SimpleMemoComponent: { + break; + } + case ClassComponent: { + if ((flags & Snapshot) !== NoFlags) { if (current !== null) { const prevProps = current.memoizedProps; const prevState = current.memoizedState; @@ -468,29 +480,35 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) { } instance.__reactInternalSnapshotBeforeUpdate = snapshot; } - break; } - case HostRoot: { + break; + } + case HostRoot: { + if ((flags & Snapshot) !== NoFlags) { if (supportsMutation) { const root = finishedWork.stateNode; clearContainer(root.containerInfo); } - break; } - case HostComponent: - case HostText: - case HostPortal: - case IncompleteClassComponent: - // Nothing to do for these component types - break; - default: { + break; + } + case HostComponent: + case HostText: + case HostPortal: + case IncompleteClassComponent: + // Nothing to do for these component types + break; + default: { + if ((flags & Snapshot) !== NoFlags) { throw new Error( 'This unit of work tag should not have side-effects. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); } } + } + if ((flags & Snapshot) !== NoFlags) { resetCurrentDebugFiberInDEV(); } } diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index b602c3691471e..2f3f9a4761b66 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -40,6 +40,7 @@ import { enableTransitionTracing, enableUseHook, enableUseMemoCacheHook, + enableUseEventHook, } from 'shared/ReactFeatureFlags'; import { REACT_CONTEXT_TYPE, @@ -85,6 +86,7 @@ import { Layout as HookLayout, Passive as HookPassive, Insertion as HookInsertion, + Snapshot as HookSnapshot, } from './ReactHookEffectTags'; import { getWorkInProgressRoot, @@ -93,6 +95,7 @@ import { requestUpdateLane, requestEventTime, markSkippedUpdateLanes, + isInvalidExecutionContextForEventFunction, } from './ReactFiberWorkLoop.new'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; @@ -1868,6 +1871,49 @@ function updateEffect( return updateEffectImpl(PassiveEffect, HookPassive, create, deps); } +function mountEvent(callback: () => T): () => T { + const hook = mountWorkInProgressHook(); + const ref = {current: callback}; + + function event() { + if (isInvalidExecutionContextForEventFunction()) { + throw new Error('An event from useEvent was called during render.'); + } + return ref.current.apply(undefined, arguments); + } + + // TODO: We don't need all the overhead of an effect object since there are no deps and no + // clean up functions. + mountEffectImpl( + UpdateEffect, + HookSnapshot, + () => { + ref.current = callback; + }, + [ref, callback], + ); + + hook.memoizedState = [ref, event]; + + return event; +} + +function updateEvent(callback: () => T): () => T { + const hook = updateWorkInProgressHook(); + const ref = hook.memoizedState[0]; + + updateEffectImpl( + UpdateEffect, + HookSnapshot, + () => { + ref.current = callback; + }, + [ref, callback], + ); + + return hook.memoizedState[1]; +} + function mountInsertionEffect( create: () => (() => void) | void, deps: Array | void | null, @@ -2546,6 +2592,9 @@ if (enableUseHook) { if (enableUseMemoCacheHook) { (ContextOnlyDispatcher: Dispatcher).useMemoCache = throwInvalidHookError; } +if (enableUseEventHook) { + (ContextOnlyDispatcher: Dispatcher).useEvent = throwInvalidHookError; +} const HooksDispatcherOnMount: Dispatcher = { readContext, @@ -2581,6 +2630,9 @@ if (enableUseHook) { if (enableUseMemoCacheHook) { (HooksDispatcherOnMount: Dispatcher).useMemoCache = useMemoCache; } +if (enableUseEventHook) { + (HooksDispatcherOnMount: Dispatcher).useEvent = mountEvent; +} const HooksDispatcherOnUpdate: Dispatcher = { readContext, @@ -2614,6 +2666,9 @@ if (enableUseMemoCacheHook) { if (enableUseHook) { (HooksDispatcherOnUpdate: Dispatcher).use = use; } +if (enableUseEventHook) { + (HooksDispatcherOnUpdate: Dispatcher).useEvent = updateEvent; +} const HooksDispatcherOnRerender: Dispatcher = { readContext, @@ -2648,6 +2703,9 @@ if (enableUseHook) { if (enableUseMemoCacheHook) { (HooksDispatcherOnRerender: Dispatcher).useMemoCache = useMemoCache; } +if (enableUseEventHook) { + (HooksDispatcherOnRerender: Dispatcher).useEvent = updateEvent; +} let HooksDispatcherOnMountInDEV: Dispatcher | null = null; let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null; @@ -2829,6 +2887,15 @@ if (__DEV__) { if (enableUseMemoCacheHook) { (HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache; } + if (enableUseEventHook) { + (HooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent( + callback: () => T, + ): () => T { + currentHookNameInDev = 'useEvent'; + mountHookTypesDev(); + return mountEvent(callback); + }; + } HooksDispatcherOnMountWithHookTypesInDEV = { readContext(context: ReactContext): T { @@ -2977,6 +3044,15 @@ if (__DEV__) { if (enableUseMemoCacheHook) { (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useMemoCache = useMemoCache; } + if (enableUseEventHook) { + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useEvent = function useEvent< + T, + >(callback: () => T): () => T { + currentHookNameInDev = 'useEvent'; + updateHookTypesDev(); + return mountEvent(callback); + }; + } HooksDispatcherOnUpdateInDEV = { readContext(context: ReactContext): T { @@ -3125,6 +3201,15 @@ if (__DEV__) { if (enableUseMemoCacheHook) { (HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache; } + if (enableUseEventHook) { + (HooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent( + callback: () => T, + ): () => T { + currentHookNameInDev = 'useEvent'; + updateHookTypesDev(); + return updateEvent(callback); + }; + } HooksDispatcherOnRerenderInDEV = { readContext(context: ReactContext): T { @@ -3274,6 +3359,15 @@ if (__DEV__) { if (enableUseMemoCacheHook) { (HooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = useMemoCache; } + if (enableUseEventHook) { + (HooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent< + T, + >(callback: () => T): () => T { + currentHookNameInDev = 'useEvent'; + updateHookTypesDev(); + return updateEvent(callback); + }; + } InvalidNestedHooksDispatcherOnMountInDEV = { readContext(context: ReactContext): T { @@ -3449,6 +3543,16 @@ if (__DEV__) { return useMemoCache(size); }; } + if (enableUseEventHook) { + (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent< + T, + >(callback: () => T): () => T { + currentHookNameInDev = 'useEvent'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountEvent(callback); + }; + } InvalidNestedHooksDispatcherOnUpdateInDEV = { readContext(context: ReactContext): T { @@ -3624,6 +3728,16 @@ if (__DEV__) { return useMemoCache(size); }; } + if (enableUseEventHook) { + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent< + T, + >(callback: () => T): () => T { + currentHookNameInDev = 'useEvent'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateEvent(callback); + }; + } InvalidNestedHooksDispatcherOnRerenderInDEV = { readContext(context: ReactContext): T { @@ -3800,4 +3914,14 @@ if (__DEV__) { return useMemoCache(size); }; } + if (enableUseEventHook) { + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent< + T, + >(callback: () => T): () => T { + currentHookNameInDev = 'useEvent'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateEvent(callback); + }; + } } diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 9f0cb1914581f..1b41711186626 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -40,6 +40,7 @@ import { enableTransitionTracing, enableUseHook, enableUseMemoCacheHook, + enableUseEventHook, } from 'shared/ReactFeatureFlags'; import { REACT_CONTEXT_TYPE, @@ -85,6 +86,7 @@ import { Layout as HookLayout, Passive as HookPassive, Insertion as HookInsertion, + Snapshot as HookSnapshot, } from './ReactHookEffectTags'; import { getWorkInProgressRoot, @@ -93,6 +95,7 @@ import { requestUpdateLane, requestEventTime, markSkippedUpdateLanes, + isInvalidExecutionContextForEventFunction, } from './ReactFiberWorkLoop.old'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; @@ -1868,6 +1871,49 @@ function updateEffect( return updateEffectImpl(PassiveEffect, HookPassive, create, deps); } +function mountEvent(callback: () => T): () => T { + const hook = mountWorkInProgressHook(); + const ref = {current: callback}; + + function event() { + if (isInvalidExecutionContextForEventFunction()) { + throw new Error('An event from useEvent was called during render.'); + } + return ref.current.apply(undefined, arguments); + } + + // TODO: We don't need all the overhead of an effect object since there are no deps and no + // clean up functions. + mountEffectImpl( + UpdateEffect, + HookSnapshot, + () => { + ref.current = callback; + }, + [ref, callback], + ); + + hook.memoizedState = [ref, event]; + + return event; +} + +function updateEvent(callback: () => T): () => T { + const hook = updateWorkInProgressHook(); + const ref = hook.memoizedState[0]; + + updateEffectImpl( + UpdateEffect, + HookSnapshot, + () => { + ref.current = callback; + }, + [ref, callback], + ); + + return hook.memoizedState[1]; +} + function mountInsertionEffect( create: () => (() => void) | void, deps: Array | void | null, @@ -2546,6 +2592,9 @@ if (enableUseHook) { if (enableUseMemoCacheHook) { (ContextOnlyDispatcher: Dispatcher).useMemoCache = throwInvalidHookError; } +if (enableUseEventHook) { + (ContextOnlyDispatcher: Dispatcher).useEvent = throwInvalidHookError; +} const HooksDispatcherOnMount: Dispatcher = { readContext, @@ -2581,6 +2630,9 @@ if (enableUseHook) { if (enableUseMemoCacheHook) { (HooksDispatcherOnMount: Dispatcher).useMemoCache = useMemoCache; } +if (enableUseEventHook) { + (HooksDispatcherOnMount: Dispatcher).useEvent = mountEvent; +} const HooksDispatcherOnUpdate: Dispatcher = { readContext, @@ -2614,6 +2666,9 @@ if (enableUseMemoCacheHook) { if (enableUseHook) { (HooksDispatcherOnUpdate: Dispatcher).use = use; } +if (enableUseEventHook) { + (HooksDispatcherOnUpdate: Dispatcher).useEvent = updateEvent; +} const HooksDispatcherOnRerender: Dispatcher = { readContext, @@ -2648,6 +2703,9 @@ if (enableUseHook) { if (enableUseMemoCacheHook) { (HooksDispatcherOnRerender: Dispatcher).useMemoCache = useMemoCache; } +if (enableUseEventHook) { + (HooksDispatcherOnRerender: Dispatcher).useEvent = updateEvent; +} let HooksDispatcherOnMountInDEV: Dispatcher | null = null; let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null; @@ -2829,6 +2887,15 @@ if (__DEV__) { if (enableUseMemoCacheHook) { (HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache; } + if (enableUseEventHook) { + (HooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent( + callback: () => T, + ): () => T { + currentHookNameInDev = 'useEvent'; + mountHookTypesDev(); + return mountEvent(callback); + }; + } HooksDispatcherOnMountWithHookTypesInDEV = { readContext(context: ReactContext): T { @@ -2977,6 +3044,15 @@ if (__DEV__) { if (enableUseMemoCacheHook) { (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useMemoCache = useMemoCache; } + if (enableUseEventHook) { + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useEvent = function useEvent< + T, + >(callback: () => T): () => T { + currentHookNameInDev = 'useEvent'; + updateHookTypesDev(); + return mountEvent(callback); + }; + } HooksDispatcherOnUpdateInDEV = { readContext(context: ReactContext): T { @@ -3125,6 +3201,15 @@ if (__DEV__) { if (enableUseMemoCacheHook) { (HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache; } + if (enableUseEventHook) { + (HooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent( + callback: () => T, + ): () => T { + currentHookNameInDev = 'useEvent'; + updateHookTypesDev(); + return updateEvent(callback); + }; + } HooksDispatcherOnRerenderInDEV = { readContext(context: ReactContext): T { @@ -3274,6 +3359,15 @@ if (__DEV__) { if (enableUseMemoCacheHook) { (HooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = useMemoCache; } + if (enableUseEventHook) { + (HooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent< + T, + >(callback: () => T): () => T { + currentHookNameInDev = 'useEvent'; + updateHookTypesDev(); + return updateEvent(callback); + }; + } InvalidNestedHooksDispatcherOnMountInDEV = { readContext(context: ReactContext): T { @@ -3449,6 +3543,16 @@ if (__DEV__) { return useMemoCache(size); }; } + if (enableUseEventHook) { + (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useEvent = function useEvent< + T, + >(callback: () => T): () => T { + currentHookNameInDev = 'useEvent'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountEvent(callback); + }; + } InvalidNestedHooksDispatcherOnUpdateInDEV = { readContext(context: ReactContext): T { @@ -3624,6 +3728,16 @@ if (__DEV__) { return useMemoCache(size); }; } + if (enableUseEventHook) { + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useEvent = function useEvent< + T, + >(callback: () => T): () => T { + currentHookNameInDev = 'useEvent'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateEvent(callback); + }; + } InvalidNestedHooksDispatcherOnRerenderInDEV = { readContext(context: ReactContext): T { @@ -3800,4 +3914,14 @@ if (__DEV__) { return useMemoCache(size); }; } + if (enableUseEventHook) { + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useEvent = function useEvent< + T, + >(callback: () => T): () => T { + currentHookNameInDev = 'useEvent'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateEvent(callback); + }; + } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index ccd1821db2c90..dc5efade1120d 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -1605,6 +1605,11 @@ export function isAlreadyRendering(): boolean { ); } +export function isInvalidExecutionContextForEventFunction() { + // Used to throw if certain APIs are called from the wrong context. + return (executionContext & RenderContext) !== NoContext; +} + export function flushControlled(fn: () => mixed): void { const prevExecutionContext = executionContext; executionContext |= BatchedContext; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index dc592a912e3a1..d8d29c496495e 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -1605,6 +1605,11 @@ export function isAlreadyRendering(): boolean { ); } +export function isInvalidExecutionContextForEventFunction() { + // Used to throw if certain APIs are called from the wrong context. + return (executionContext & RenderContext) !== NoContext; +} + export function flushControlled(fn: () => mixed): void { const prevExecutionContext = executionContext; executionContext |= BatchedContext; diff --git a/packages/react-reconciler/src/ReactHookEffectTags.js b/packages/react-reconciler/src/ReactHookEffectTags.js index 54be635a623a9..41b3d6be761ee 100644 --- a/packages/react-reconciler/src/ReactHookEffectTags.js +++ b/packages/react-reconciler/src/ReactHookEffectTags.js @@ -9,12 +9,13 @@ export type HookFlags = number; -export const NoFlags = /* */ 0b0000; +export const NoFlags = /* */ 0b00000; // Represents whether effect should fire. -export const HasEffect = /* */ 0b0001; +export const HasEffect = /* */ 0b00001; // Represents the phase in which the effect (not the clean-up) fires. -export const Insertion = /* */ 0b0010; -export const Layout = /* */ 0b0100; -export const Passive = /* */ 0b1000; +export const Snapshot = /* */ 0b00010; +export const Insertion = /* */ 0b00100; +export const Layout = /* */ 0b01000; +export const Passive = /* */ 0b10000; diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 004aa17e56ad8..c7ee97c69c9e3 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -42,6 +42,7 @@ export type HookType = | 'useContext' | 'useRef' | 'useEffect' + | 'useEvent' | 'useInsertionEffect' | 'useLayoutEffect' | 'useCallback' @@ -376,6 +377,7 @@ export type Dispatcher = { create: () => (() => void) | void, deps: Array | void | null, ): void, + useEvent?: (callback: () => T) => () => T, useInsertionEffect( create: () => (() => void) | void, deps: Array | void | null, diff --git a/packages/react-reconciler/src/__tests__/useEvent-test.js b/packages/react-reconciler/src/__tests__/useEvent-test.js new file mode 100644 index 0000000000000..39c0f9f316ef3 --- /dev/null +++ b/packages/react-reconciler/src/__tests__/useEvent-test.js @@ -0,0 +1,668 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + * @jest-environment node + */ + +/* eslint-disable no-func-assign */ + +'use strict'; + +import {useInsertionEffect} from 'react'; + +describe('useEvent', () => { + let React; + let ReactNoop; + let Scheduler; + let act; + let createContext; + let useContext; + let useState; + let useEvent; + let useEffect; + let useLayoutEffect; + let useMemo; + + beforeEach(() => { + React = require('react'); + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + + act = require('jest-react').act; + createContext = React.createContext; + useContext = React.useContext; + useState = React.useState; + useEvent = React.experimental_useEvent; + useEffect = React.useEffect; + useLayoutEffect = React.useLayoutEffect; + useMemo = React.useMemo; + }); + + function span(prop) { + return {type: 'span', hidden: false, children: [], prop}; + } + + function Text(props) { + Scheduler.unstable_yieldValue(props.text); + return ; + } + + // @gate enableUseEventHook + it('memoizes basic case correctly', () => { + class IncrementButton extends React.PureComponent { + increment = () => { + this.props.onClick(); + }; + render() { + return ; + } + } + + function Counter({incrementBy}) { + const [count, updateCount] = useState(0); + const onClick = useEvent(() => updateCount(c => c + incrementBy)); + + return ( + <> + onClick()} ref={button} /> + + + ); + } + + const button = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Increment', 'Count: 0']); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 0'), + ]); + + act(button.current.increment); + expect(Scheduler).toHaveYielded(['Increment', 'Count: 1']); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 1'), + ]); + + act(button.current.increment); + expect(Scheduler).toHaveYielded([ + 'Increment', + // Event should use the updated callback function closed over the new value. + 'Count: 2', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 2'), + ]); + + // Increase the increment prop amount + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Increment', 'Count: 2']); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 2'), + ]); + + // Event uses the new prop + act(button.current.increment); + expect(Scheduler).toHaveYielded(['Increment', 'Count: 12']); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 12'), + ]); + }); + + // @gate enableUseEventHook + it('does not preserve `this` in event functions', () => { + class GreetButton extends React.PureComponent { + greet = () => { + this.props.onClick(); + }; + render() { + return ; + } + } + function Greeter({hello}) { + const person = { + toString() { + return 'Jane'; + }, + greet() { + return updateGreeting(this + ' says ' + hello); + }, + }; + const [greeting, updateGreeting] = useState('Seb says ' + hello); + const onClick = useEvent(person.greet); + + return ( + <> + onClick()} ref={button} /> + + + ); + } + + const button = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Say hej', 'Greeting: Seb says hej']); + expect(ReactNoop.getChildren()).toEqual([ + span('Say hej'), + span('Greeting: Seb says hej'), + ]); + + act(button.current.greet); + expect(Scheduler).toHaveYielded([ + 'Say hej', + 'Greeting: undefined says hej', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Say hej'), + span('Greeting: undefined says hej'), + ]); + }); + + // @gate enableUseEventHook + it('throws when called in render', () => { + class IncrementButton extends React.PureComponent { + increment = () => { + this.props.onClick(); + }; + + render() { + // Will throw. + this.props.onClick(); + + return ; + } + } + + function Counter({incrementBy}) { + const [count, updateCount] = useState(0); + const onClick = useEvent(() => updateCount(c => c + incrementBy)); + + return ( + <> + onClick()} /> + + + ); + } + + ReactNoop.render(); + expect(Scheduler).toFlushAndThrow( + 'An event from useEvent was called during render', + ); + + // If something throws, we try one more time synchronously in case the error was + // caused by a data race. See recoverFromConcurrentError + expect(Scheduler).toHaveYielded(['Count: 0', 'Count: 0']); + }); + + // @gate enableUseEventHook + it("useLayoutEffect shouldn't re-fire when event handlers change", () => { + class IncrementButton extends React.PureComponent { + increment = () => { + this.props.onClick(); + }; + render() { + return ; + } + } + + function Counter({incrementBy}) { + const [count, updateCount] = useState(0); + const increment = useEvent(amount => + updateCount(c => c + (amount || incrementBy)), + ); + + useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Effect: by ' + incrementBy * 2); + increment(incrementBy * 2); + }, [incrementBy]); + + return ( + <> + increment()} ref={button} /> + + + ); + } + + const button = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toHaveYielded([]); + expect(Scheduler).toFlushAndYield([ + 'Increment', + 'Count: 0', + 'Effect: by 2', + 'Increment', + 'Count: 2', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 2'), + ]); + + act(button.current.increment); + expect(Scheduler).toHaveYielded([ + 'Increment', + // Effect should not re-run because the dependency hasn't changed. + 'Count: 3', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 3'), + ]); + + act(button.current.increment); + expect(Scheduler).toHaveYielded([ + 'Increment', + // Event should use the updated callback function closed over the new value. + 'Count: 4', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 4'), + ]); + + // Increase the increment prop amount + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'Increment', + 'Count: 4', + 'Effect: by 20', + 'Increment', + 'Count: 24', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 24'), + ]); + + // Event uses the new prop + act(button.current.increment); + expect(Scheduler).toHaveYielded(['Increment', 'Count: 34']); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 34'), + ]); + }); + + // @gate enableUseEventHook + it("useEffect shouldn't re-fire when event handlers change", () => { + class IncrementButton extends React.PureComponent { + increment = () => { + this.props.onClick(); + }; + render() { + return ; + } + } + + function Counter({incrementBy}) { + const [count, updateCount] = useState(0); + const increment = useEvent(amount => + updateCount(c => c + (amount || incrementBy)), + ); + + useEffect(() => { + Scheduler.unstable_yieldValue('Effect: by ' + incrementBy * 2); + increment(incrementBy * 2); + }, [incrementBy]); + + return ( + <> + increment()} ref={button} /> + + + ); + } + + const button = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'Increment', + 'Count: 0', + 'Effect: by 2', + 'Increment', + 'Count: 2', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 2'), + ]); + + act(button.current.increment); + expect(Scheduler).toHaveYielded([ + 'Increment', + // Effect should not re-run because the dependency hasn't changed. + 'Count: 3', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 3'), + ]); + + act(button.current.increment); + expect(Scheduler).toHaveYielded([ + 'Increment', + // Event should use the updated callback function closed over the new value. + 'Count: 4', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 4'), + ]); + + // Increase the increment prop amount + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'Increment', + 'Count: 4', + 'Effect: by 20', + 'Increment', + 'Count: 24', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 24'), + ]); + + // Event uses the new prop + act(button.current.increment); + expect(Scheduler).toHaveYielded(['Increment', 'Count: 34']); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 34'), + ]); + }); + + // @gate enableUseEventHook + it('is stable in a custom hook', () => { + class IncrementButton extends React.PureComponent { + increment = () => { + this.props.onClick(); + }; + render() { + return ; + } + } + + function useCount(incrementBy) { + const [count, updateCount] = useState(0); + const increment = useEvent(amount => + updateCount(c => c + (amount || incrementBy)), + ); + + return [count, increment]; + } + + function Counter({incrementBy}) { + const [count, increment] = useCount(incrementBy); + + useEffect(() => { + Scheduler.unstable_yieldValue('Effect: by ' + incrementBy * 2); + increment(incrementBy * 2); + }, [incrementBy]); + + return ( + <> + increment()} ref={button} /> + + + ); + } + + const button = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'Increment', + 'Count: 0', + 'Effect: by 2', + 'Increment', + 'Count: 2', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 2'), + ]); + + act(button.current.increment); + expect(Scheduler).toHaveYielded([ + 'Increment', + // Effect should not re-run because the dependency hasn't changed. + 'Count: 3', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 3'), + ]); + + act(button.current.increment); + expect(Scheduler).toHaveYielded([ + 'Increment', + // Event should use the updated callback function closed over the new value. + 'Count: 4', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 4'), + ]); + + // Increase the increment prop amount + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'Increment', + 'Count: 4', + 'Effect: by 20', + 'Increment', + 'Count: 24', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 24'), + ]); + + // Event uses the new prop + act(button.current.increment); + expect(Scheduler).toHaveYielded(['Increment', 'Count: 34']); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 34'), + ]); + }); + + // @gate enableUseEventHook + it('is mutated before all other effects', () => { + function Counter({value}) { + useInsertionEffect(() => { + Scheduler.unstable_yieldValue('Effect value: ' + value); + increment(); + }, [value]); + + // This is defined after the insertion effect, but it should + // update the event fn _before_ the insertion effect fires. + const increment = useEvent(() => { + Scheduler.unstable_yieldValue('Event value: ' + value); + }); + + return <>; + } + + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Effect value: 1', 'Event value: 1']); + + act(() => ReactNoop.render()); + expect(Scheduler).toHaveYielded(['Effect value: 2', 'Event value: 2']); + }); + + // @gate enableUseEventHook + it('integration: implements docs chat room example', () => { + function createConnection() { + let connectedCallback; + let timeout; + return { + connect() { + timeout = setTimeout(() => { + if (connectedCallback) { + connectedCallback(); + } + }, 100); + }, + on(event, callback) { + if (connectedCallback) { + throw Error('Cannot add the handler twice.'); + } + if (event !== 'connected') { + throw Error('Only "connected" event is supported.'); + } + connectedCallback = callback; + }, + disconnect() { + clearTimeout(timeout); + }, + }; + } + + function ChatRoom({roomId, theme}) { + const onConnected = useEvent(() => { + Scheduler.unstable_yieldValue('Connected! theme: ' + theme); + }); + + useEffect(() => { + const connection = createConnection(roomId); + connection.on('connected', () => { + onConnected(); + }); + connection.connect(); + return () => connection.disconnect(); + }, [roomId, onConnected]); + + return ; + } + + act(() => ReactNoop.render()); + expect(Scheduler).toHaveYielded(['Welcome to the general room!']); + expect(ReactNoop.getChildren()).toEqual([ + span('Welcome to the general room!'), + ]); + + jest.advanceTimersByTime(100); + Scheduler.unstable_advanceTime(100); + expect(Scheduler).toHaveYielded(['Connected! theme: light']); + + // change roomId only + act(() => ReactNoop.render()); + expect(Scheduler).toHaveYielded(['Welcome to the music room!']); + expect(ReactNoop.getChildren()).toEqual([ + span('Welcome to the music room!'), + ]); + jest.advanceTimersByTime(100); + Scheduler.unstable_advanceTime(100); + // should trigger a reconnect + expect(Scheduler).toHaveYielded(['Connected! theme: light']); + + // change theme only + act(() => ReactNoop.render()); + expect(Scheduler).toHaveYielded(['Welcome to the music room!']); + expect(ReactNoop.getChildren()).toEqual([ + span('Welcome to the music room!'), + ]); + jest.advanceTimersByTime(100); + Scheduler.unstable_advanceTime(100); + // should not trigger a reconnect + expect(Scheduler).toFlushWithoutYielding(); + + // change roomId only + act(() => ReactNoop.render()); + expect(Scheduler).toHaveYielded(['Welcome to the travel room!']); + expect(ReactNoop.getChildren()).toEqual([ + span('Welcome to the travel room!'), + ]); + jest.advanceTimersByTime(100); + Scheduler.unstable_advanceTime(100); + // should trigger a reconnect + expect(Scheduler).toHaveYielded(['Connected! theme: dark']); + }); + + // @gate enableUseEventHook + it('integration: implements the docs logVisit example', () => { + class AddToCartButton extends React.PureComponent { + addToCart = () => { + this.props.onClick(); + }; + render() { + return ; + } + } + const ShoppingCartContext = createContext(null); + + function AppShell({children}) { + const [items, updateItems] = useState([]); + const value = useMemo(() => ({items, updateItems}), [items, updateItems]); + + return ( + + {children} + + ); + } + + function Page({url}) { + const {items, updateItems} = useContext(ShoppingCartContext); + const onClick = useEvent(() => updateItems([...items, 1])); + const numberOfItems = items.length; + + const onVisit = useEvent(visitedUrl => { + Scheduler.unstable_yieldValue( + 'url: ' + url + ', numberOfItems: ' + numberOfItems, + ); + }); + + useEffect(() => { + onVisit(url); + }, [url]); + + return ( + { + onClick(); + }} + ref={button} + /> + ); + } + + const button = React.createRef(null); + act(() => + ReactNoop.render( + + + , + ), + ); + expect(Scheduler).toHaveYielded([ + 'Add to cart', + 'url: /shop/1, numberOfItems: 0', + ]); + act(button.current.addToCart); + expect(Scheduler).toHaveYielded(['Add to cart']); + + act(() => + ReactNoop.render( + + + , + ), + ); + expect(Scheduler).toHaveYielded([ + 'Add to cart', + 'url: /shop/2, numberOfItems: 1', + ]); + }); +}); diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index 0bc75a3531681..b18fc3d7ad30f 100644 --- a/packages/react/index.classic.fb.js +++ b/packages/react/index.classic.fb.js @@ -51,6 +51,7 @@ export { useDeferredValue, useDeferredValue as unstable_useDeferredValue, // TODO: Remove once call sights updated to useDeferredValue useEffect, + experimental_useEvent, useImperativeHandle, useLayoutEffect, useInsertionEffect, diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index d60351e263981..2d36e38836144 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -43,6 +43,7 @@ export { useDebugValue, useDeferredValue, useEffect, + experimental_useEvent, useImperativeHandle, useInsertionEffect, useLayoutEffect, diff --git a/packages/react/index.js b/packages/react/index.js index d0628ab003a79..9db87fa8921ab 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -71,6 +71,7 @@ export { useDebugValue, useDeferredValue, useEffect, + experimental_useEvent, useImperativeHandle, useInsertionEffect, useLayoutEffect, diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index 24de7511daed5..ad440565309f8 100644 --- a/packages/react/index.modern.fb.js +++ b/packages/react/index.modern.fb.js @@ -49,6 +49,7 @@ export { useDeferredValue, useDeferredValue as unstable_useDeferredValue, // TODO: Remove once call sights updated to useDeferredValue useEffect, + experimental_useEvent, useImperativeHandle, useInsertionEffect, useLayoutEffect, diff --git a/packages/react/index.stable.js b/packages/react/index.stable.js index 3ed868197b6f8..ea6691f68f8ca 100644 --- a/packages/react/index.stable.js +++ b/packages/react/index.stable.js @@ -33,6 +33,7 @@ export { useDebugValue, useDeferredValue, useEffect, + experimental_useEvent, useImperativeHandle, useInsertionEffect, useLayoutEffect, diff --git a/packages/react/src/React.js b/packages/react/src/React.js index fae7ee56b758e..fc6c77cbb6539 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -41,6 +41,7 @@ import { useCallback, useContext, useEffect, + useEvent, useImperativeHandle, useDebugValue, useInsertionEffect, @@ -96,6 +97,7 @@ export { useCallback, useContext, useEffect, + useEvent as experimental_useEvent, useImperativeHandle, useDebugValue, useInsertionEffect, diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 7192f9aea5e39..2e5046353a3a4 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -217,3 +217,9 @@ export function useMemoCache(size: number): Array { // $FlowFixMe This is unstable, thus optional return dispatcher.useMemoCache(size); } + +export function useEvent(callback: T): void { + const dispatcher = resolveDispatcher(); + // $FlowFixMe This is unstable, thus optional + return dispatcher.useEvent(callback); +} diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index ba7fcb61a613c..447d25542536e 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -120,6 +120,8 @@ export const enableUseHook = __EXPERIMENTAL__; // auto-memoization. export const enableUseMemoCacheHook = __EXPERIMENTAL__; +export const enableUseEventHook = __EXPERIMENTAL__; + // ----------------------------------------------------------------------------- // Chopping Block // diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 76f19551875ba..500c4bde865fa 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -52,6 +52,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = true; export const enableUseHook = false; export const enableUseMemoCacheHook = false; +export const enableUseEventHook = false; export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true; export const enableClientRenderFallbackOnTextMismatch = true; export const enableComponentStackLocations = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 6de6388896c23..fb889cc65e736 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -42,6 +42,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; export const enableUseHook = false; export const enableUseMemoCacheHook = false; +export const enableUseEventHook = false; export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true; export const enableClientRenderFallbackOnTextMismatch = true; export const enableComponentStackLocations = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 1ac2ff4f2acf0..53ed7ca5a14c6 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -42,6 +42,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; export const enableUseHook = false; export const enableUseMemoCacheHook = false; +export const enableUseEventHook = false; export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true; export const enableClientRenderFallbackOnTextMismatch = true; export const enableComponentStackLocations = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 8b3d4e230d438..034a605979ab1 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -51,6 +51,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; export const enableUseHook = false; export const enableUseMemoCacheHook = false; +export const enableUseEventHook = false; export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true; export const enableClientRenderFallbackOnTextMismatch = true; export const enableStrictEffects = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index e414784563f3c..239f67b9d54ae 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -42,6 +42,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; export const enableUseHook = false; export const enableUseMemoCacheHook = false; +export const enableUseEventHook = false; export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true; export const enableClientRenderFallbackOnTextMismatch = true; export const enableComponentStackLocations = true; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index 0d719ca3523d2..6b078b93c26f7 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -42,6 +42,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; export const enableUseHook = false; export const enableUseMemoCacheHook = false; +export const enableUseEventHook = false; export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true; export const enableClientRenderFallbackOnTextMismatch = true; export const enableComponentStackLocations = true; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 6d2cd32d9f86b..b16916483b3fd 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -42,6 +42,7 @@ export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = true; export const enableUseHook = false; export const enableUseMemoCacheHook = false; +export const enableUseEventHook = false; export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = true; export const enableClientRenderFallbackOnTextMismatch = true; export const enableComponentStackLocations = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 18a333e40f769..605282c150c9b 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -55,6 +55,7 @@ export const enableCPUSuspense = true; export const enableFloat = false; export const enableUseHook = true; export const enableUseMemoCacheHook = true; +export const enableUseEventHook = true; // Logs additional User Timing API marks for use with an experimental profiling tool. export const enableSchedulingProfiler: boolean = diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 4817eadd99031..7a23215cc332a 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -424,5 +424,6 @@ "436": "Stylesheet resources need a unique representation in the DOM while hydrating and more than one matching DOM Node was found. To fix, ensure you are only rendering one stylesheet link with an href attribute of \"%s\".", "437": "the \"precedence\" prop for links to stylesheets expects to receive a string but received something of type \"%s\" instead.", "438": "An unsupported type was passed to use(): %s", - "439": "We didn't expect to see a forward reference. This is a bug in the React Server." + "439": "We didn't expect to see a forward reference. This is a bug in the React Server.", + "440": "An event from useEvent was called during render." }