From 9209c30ff98528cfbbe9df0774fda0b117975a25 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 24 Feb 2021 16:14:14 -0500 Subject: [PATCH] Add StrictMode level prop and createRoot unstable_strictModeLevel option (#20849) * The exported '' tag remains the same and opts legacy subtrees into strict mode level one ('mode == StrictModeL1'). This mode enables DEV-only double rendering, double component lifecycles, string ref warnings, legacy context warnings, etc. The primary purpose of this mode is to help detected render phase side effects. No new behavior. Roots created with experimental 'createRoot' and 'createBlockingRoot' APIs will also (for now) continue to default to strict mode level 1. In a subsequent commit I will add support for a 'level' attribute on the '' tag (as well as a new option supported by ). This will be the way to opt into strict mode level 2 ('mode == StrictModeL2'). This mode will enable DEV-only double invoking of effects on initial mount. This will simulate future Offscreen API semantics for trees being mounted, then hidden, and then shown again. The primary purpose of this mode is to enable applications to prepare for compatibility with the new Offscreen API (more information to follow shortly). For now, this commit changes no public facing behavior. The only mechanism for opting into strict mode level 2 is the pre-existing 'enableDoubleInvokingEffects' feature flag (only enabled within Facebook for now). * Renamed strict mode constants StrictModeL1 -> StrictLegacyMode and StrictModeL2 -> StrictEffectsMode * Renamed tests * Split strict effects mode into two flags One flag ('enableStrictEffects') enables strict mode level 2. It is similar to 'debugRenderPhaseSideEffectsForStrictMode' which enables srtict mode level 1. The second flag ('createRootStrictEffectsByDefault') controls the default strict mode level for 'createRoot' trees. For now, all 'createRoot' trees remain level 1 by default. We will experiment with level 2 within Facebook. This is a prerequisite for adding a configurable option to 'createRoot' that enables choosing a different StrictMode level than the default. * Add StrictMode 'unstable_level' prop and createRoot 'unstable_strictModeLevel' option New StrictMode 'unstable_level' prop allows specifying which level of strict mode to use. If no level attribute is specified, StrictLegacyMode will be used to maintain backwards compatibility. Otherwise the following is true: * Level 0 does nothing * Level 1 selects StrictLegacyMode * Level 2 selects StrictEffectsMode (which includes StrictLegacyMode) Levels can be increased with nesting (0 -> 1 -> 2) but not decreased. This commit also adds a new 'unstable_strictModeLevel' option to the createRoot and createBatchedRoot APIs. This option can be used to override default behavior to increase or decrease the StrictMode level of the root. A subsequent commit will add additional DEV warnings: * If a nested StrictMode tag attempts to explicitly decrease the level * If a level attribute changes in an update --- packages/react-dom/src/client/ReactDOMRoot.js | 14 +- .../react-native-renderer/src/ReactFabric.js | 2 +- .../src/ReactNativeRenderer.js | 2 +- .../src/createReactNoop.js | 5 +- .../src/ReactChildFiber.new.js | 4 +- .../src/ReactChildFiber.old.js | 4 +- .../react-reconciler/src/ReactFiber.new.js | 64 +- .../react-reconciler/src/ReactFiber.old.js | 64 +- .../src/ReactFiberBeginWork.new.js | 12 +- .../src/ReactFiberBeginWork.old.js | 12 +- .../src/ReactFiberClassComponent.new.js | 31 +- .../src/ReactFiberClassComponent.old.js | 31 +- .../src/ReactFiberCommitWork.new.js | 26 +- .../src/ReactFiberCommitWork.old.js | 26 +- .../src/ReactFiberHooks.new.js | 26 +- .../src/ReactFiberHooks.old.js | 26 +- .../src/ReactFiberReconciler.new.js | 15 +- .../src/ReactFiberReconciler.old.js | 15 +- .../src/ReactFiberRoot.new.js | 3 +- .../src/ReactFiberRoot.old.js | 3 +- .../src/ReactFiberWorkLoop.new.js | 25 +- .../src/ReactFiberWorkLoop.old.js | 25 +- .../src/ReactStrictModeWarnings.new.js | 10 +- .../src/ReactStrictModeWarnings.old.js | 10 +- .../react-reconciler/src/ReactTypeOfMode.js | 16 +- .../src/ReactUpdateQueue.new.js | 6 +- .../src/ReactUpdateQueue.old.js | 6 +- .../ReactDoubleInvokeEvents-test.internal.js | 747 ------------------ ...ents-test.js => StrictEffectsMode-test.js} | 5 +- ...StrictEffectsModeDefaults-test.internal.js | 635 +++++++++++++++ .../src/ReactTestRenderer.js | 1 + .../__tests__/ReactProfiler-test.internal.js | 3 +- .../ReactStrictMode-test.internal.js | 264 +++++++ packages/shared/ReactFeatureFlags.js | 12 +- .../forks/ReactFeatureFlags.native-fb.js | 3 +- .../forks/ReactFeatureFlags.native-oss.js | 3 +- .../forks/ReactFeatureFlags.test-renderer.js | 3 +- .../ReactFeatureFlags.test-renderer.native.js | 3 +- .../ReactFeatureFlags.test-renderer.www.js | 3 +- .../shared/forks/ReactFeatureFlags.testing.js | 3 +- .../forks/ReactFeatureFlags.testing.www.js | 3 +- .../forks/ReactFeatureFlags.www-dynamic.js | 3 +- .../shared/forks/ReactFeatureFlags.www.js | 3 +- 43 files changed, 1243 insertions(+), 934 deletions(-) delete mode 100644 packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.internal.js rename packages/react-reconciler/src/__tests__/{ReactDoubleInvokeEvents-test.js => StrictEffectsMode-test.js} (99%) create mode 100644 packages/react-reconciler/src/__tests__/StrictEffectsModeDefaults-test.internal.js create mode 100644 packages/react/src/__tests__/ReactStrictMode-test.internal.js diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js index 66a31328540e5..62a72dc229618 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -27,6 +27,7 @@ export type RootOptions = { mutableSources?: Array>, ... }, + unstable_strictModeLevel?: number, ... }; @@ -128,7 +129,18 @@ function createRootImpl( options.hydrationOptions != null && options.hydrationOptions.mutableSources) || null; - const root = createContainer(container, tag, hydrate, hydrationCallbacks); + const strictModeLevelOverride = + options != null && options.unstable_strictModeLevel != null + ? options.unstable_strictModeLevel + : null; + + const root = createContainer( + container, + tag, + hydrate, + hydrationCallbacks, + strictModeLevelOverride, + ); markContainerAsRoot(root.current, container); const rootContainerElement = diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js index 87fe4e8e2bf1f..03ad183c37df4 100644 --- a/packages/react-native-renderer/src/ReactFabric.js +++ b/packages/react-native-renderer/src/ReactFabric.js @@ -203,7 +203,7 @@ function render( if (!root) { // TODO (bvaughn): If we decide to keep the wrapper component, // We could create a wrapper for containerTag as well to reduce special casing. - root = createContainer(containerTag, LegacyRoot, false, null); + root = createContainer(containerTag, LegacyRoot, false, null, null); roots.set(containerTag, root); } updateContainer(element, root, null, callback); diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index fdf9ebf5ac597..673482d0175cd 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -202,7 +202,7 @@ function render( if (!root) { // TODO (bvaughn): If we decide to keep the wrapper component, // We could create a wrapper for containerTag as well to reduce special casing. - root = createContainer(containerTag, LegacyRoot, false, null); + root = createContainer(containerTag, LegacyRoot, false, null, null); roots.set(containerTag, root); } updateContainer(element, root, null, callback); diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 26c58ffd8445c..743470966b3f0 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -722,7 +722,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { if (!root) { const container = {rootID: rootID, pendingChildren: [], children: []}; rootContainers.set(rootID, container); - root = NoopRenderer.createContainer(container, tag, false, null); + root = NoopRenderer.createContainer(container, tag, false, null, null); roots.set(rootID, root); } return root.current.stateNode.containerInfo; @@ -740,6 +740,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { ConcurrentRoot, false, null, + null, ); return { _Scheduler: Scheduler, @@ -766,6 +767,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { BlockingRoot, false, null, + null, ); return { _Scheduler: Scheduler, @@ -792,6 +794,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { LegacyRoot, false, null, + null, ); return { _Scheduler: Scheduler, diff --git a/packages/react-reconciler/src/ReactChildFiber.new.js b/packages/react-reconciler/src/ReactChildFiber.new.js index 3435694b7ab0a..222a8730c4fa2 100644 --- a/packages/react-reconciler/src/ReactChildFiber.new.js +++ b/packages/react-reconciler/src/ReactChildFiber.new.js @@ -46,7 +46,7 @@ import { } from './ReactFiber.new'; import {emptyRefsObject} from './ReactFiberClassComponent.new'; import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.new'; -import {StrictMode} from './ReactTypeOfMode'; +import {StrictLegacyMode} from './ReactTypeOfMode'; let didWarnAboutMaps; let didWarnAboutGenerators; @@ -114,7 +114,7 @@ function coerceRef( // TODO: Clean this up once we turn on the string ref warning for // everyone, because the strict mode case will no longer be relevant if ( - (returnFiber.mode & StrictMode || warnAboutStringRefs) && + (returnFiber.mode & StrictLegacyMode || warnAboutStringRefs) && // We warn in ReactElement.js if owner and self are equal for string refs // because these cannot be automatically converted to an arrow function // using a codemod. Therefore, we don't have to warn about string refs again. diff --git a/packages/react-reconciler/src/ReactChildFiber.old.js b/packages/react-reconciler/src/ReactChildFiber.old.js index 7a4ab0b172085..036fa65503c04 100644 --- a/packages/react-reconciler/src/ReactChildFiber.old.js +++ b/packages/react-reconciler/src/ReactChildFiber.old.js @@ -46,7 +46,7 @@ import { } from './ReactFiber.old'; import {emptyRefsObject} from './ReactFiberClassComponent.old'; import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.old'; -import {StrictMode} from './ReactTypeOfMode'; +import {StrictLegacyMode} from './ReactTypeOfMode'; let didWarnAboutMaps; let didWarnAboutGenerators; @@ -114,7 +114,7 @@ function coerceRef( // TODO: Clean this up once we turn on the string ref warning for // everyone, because the strict mode case will no longer be relevant if ( - (returnFiber.mode & StrictMode || warnAboutStringRefs) && + (returnFiber.mode & StrictLegacyMode || warnAboutStringRefs) && // We warn in ReactElement.js if owner and self are equal for string refs // because these cannot be automatically converted to an arrow function // using a codemod. Therefore, we don't have to warn about string refs again. diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js index 983855ced5685..f1a0f56b23409 100644 --- a/packages/react-reconciler/src/ReactFiber.new.js +++ b/packages/react-reconciler/src/ReactFiber.new.js @@ -19,9 +19,11 @@ import type {OffscreenProps} from './ReactFiberOffscreenComponent'; import invariant from 'shared/invariant'; import { + createRootStrictEffectsByDefault, + enableCache, + enableStrictEffects, enableProfilerTimer, enableScopeAPI, - enableCache, } from 'shared/ReactFeatureFlags'; import {NoFlags, Placement, StaticMask} from './ReactFiberFlags'; import {ConcurrentRoot, BlockingRoot} from './ReactRootTags'; @@ -64,7 +66,8 @@ import { ConcurrentMode, DebugTracingMode, ProfileMode, - StrictMode, + StrictLegacyMode, + StrictEffectsMode, BlockingMode, } from './ReactTypeOfMode'; import { @@ -418,12 +421,47 @@ export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) { return workInProgress; } -export function createHostRootFiber(tag: RootTag): Fiber { +export function createHostRootFiber( + tag: RootTag, + strictModeLevelOverride: null | number, +): Fiber { let mode; if (tag === ConcurrentRoot) { - mode = ConcurrentMode | BlockingMode | StrictMode; + mode = ConcurrentMode | BlockingMode; + if (strictModeLevelOverride !== null) { + if (strictModeLevelOverride >= 1) { + mode |= StrictLegacyMode; + } + if (enableStrictEffects) { + if (strictModeLevelOverride >= 2) { + mode |= StrictEffectsMode; + } + } + } else { + if (enableStrictEffects && createRootStrictEffectsByDefault) { + mode |= StrictLegacyMode | StrictEffectsMode; + } else { + mode |= StrictLegacyMode; + } + } } else if (tag === BlockingRoot) { - mode = BlockingMode | StrictMode; + mode = BlockingMode; + if (strictModeLevelOverride !== null) { + if (strictModeLevelOverride >= 1) { + mode |= StrictLegacyMode; + } + if (enableStrictEffects) { + if (strictModeLevelOverride >= 2) { + mode |= StrictEffectsMode; + } + } + } else { + if (enableStrictEffects && createRootStrictEffectsByDefault) { + mode |= StrictLegacyMode | StrictEffectsMode; + } else { + mode |= StrictLegacyMode; + } + } } else { mode = NoMode; } @@ -472,7 +510,21 @@ export function createFiberFromTypeAndProps( break; case REACT_STRICT_MODE_TYPE: fiberTag = Mode; - mode |= StrictMode; + + // Legacy strict mode ( without any level prop) defaults to level 1. + const level = + pendingProps.unstable_level == null ? 1 : pendingProps.unstable_level; + + // Levels cascade; higher levels inherit all lower level modes. + // It is explicitly not supported to lower a mode with nesting, only to increase it. + if (level >= 1) { + mode |= StrictLegacyMode; + } + if (enableStrictEffects) { + if (level >= 2) { + mode |= StrictEffectsMode; + } + } break; case REACT_PROFILER_TYPE: return createFiberFromProfiler(pendingProps, mode, lanes, key); diff --git a/packages/react-reconciler/src/ReactFiber.old.js b/packages/react-reconciler/src/ReactFiber.old.js index ed2ae45f4ec17..6419fd6b261b3 100644 --- a/packages/react-reconciler/src/ReactFiber.old.js +++ b/packages/react-reconciler/src/ReactFiber.old.js @@ -19,9 +19,11 @@ import type {OffscreenProps} from './ReactFiberOffscreenComponent'; import invariant from 'shared/invariant'; import { + createRootStrictEffectsByDefault, + enableCache, + enableStrictEffects, enableProfilerTimer, enableScopeAPI, - enableCache, } from 'shared/ReactFeatureFlags'; import {NoFlags, Placement, StaticMask} from './ReactFiberFlags'; import {ConcurrentRoot, BlockingRoot} from './ReactRootTags'; @@ -64,7 +66,8 @@ import { ConcurrentMode, DebugTracingMode, ProfileMode, - StrictMode, + StrictLegacyMode, + StrictEffectsMode, BlockingMode, } from './ReactTypeOfMode'; import { @@ -418,12 +421,47 @@ export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) { return workInProgress; } -export function createHostRootFiber(tag: RootTag): Fiber { +export function createHostRootFiber( + tag: RootTag, + strictModeLevelOverride: null | number, +): Fiber { let mode; if (tag === ConcurrentRoot) { - mode = ConcurrentMode | BlockingMode | StrictMode; + mode = ConcurrentMode | BlockingMode; + if (strictModeLevelOverride !== null) { + if (strictModeLevelOverride >= 1) { + mode |= StrictLegacyMode; + } + if (enableStrictEffects) { + if (strictModeLevelOverride >= 2) { + mode |= StrictEffectsMode; + } + } + } else { + if (enableStrictEffects && createRootStrictEffectsByDefault) { + mode |= StrictLegacyMode | StrictEffectsMode; + } else { + mode |= StrictLegacyMode; + } + } } else if (tag === BlockingRoot) { - mode = BlockingMode | StrictMode; + mode = BlockingMode; + if (strictModeLevelOverride !== null) { + if (strictModeLevelOverride >= 1) { + mode |= StrictLegacyMode; + } + if (enableStrictEffects) { + if (strictModeLevelOverride >= 2) { + mode |= StrictEffectsMode; + } + } + } else { + if (enableStrictEffects && createRootStrictEffectsByDefault) { + mode |= StrictLegacyMode | StrictEffectsMode; + } else { + mode |= StrictLegacyMode; + } + } } else { mode = NoMode; } @@ -472,7 +510,21 @@ export function createFiberFromTypeAndProps( break; case REACT_STRICT_MODE_TYPE: fiberTag = Mode; - mode |= StrictMode; + + // Legacy strict mode ( without any level prop) defaults to level 1. + const level = + pendingProps.unstable_level == null ? 1 : pendingProps.unstable_level; + + // Levels cascade; higher levels inherit all lower level modes. + // It is explicitly not supported to lower a mode with nesting, only to increase it. + if (level >= 1) { + mode |= StrictLegacyMode; + } + if (enableStrictEffects) { + if (level >= 2) { + mode |= StrictEffectsMode; + } + } break; case REACT_PROFILER_TYPE: return createFiberFromProfiler(pendingProps, mode, lanes, key); diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 6537d103e2324..6dffcc328927a 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -125,7 +125,7 @@ import { ConcurrentMode, NoMode, ProfileMode, - StrictMode, + StrictLegacyMode, BlockingMode, } from './ReactTypeOfMode'; import { @@ -357,7 +357,7 @@ function updateForwardRef( ); if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { @@ -889,7 +889,7 @@ function updateFunctionComponent( ); if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { @@ -1068,7 +1068,7 @@ function finishClassComponent( nextChildren = instance.render(); if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { @@ -1478,7 +1478,7 @@ function mountIndeterminateComponent( } } - if (workInProgress.mode & StrictMode) { + if (workInProgress.mode & StrictLegacyMode) { ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, null); } @@ -1615,7 +1615,7 @@ function mountIndeterminateComponent( if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index 4c0594329d94e..7801afe9fa231 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -125,7 +125,7 @@ import { ConcurrentMode, NoMode, ProfileMode, - StrictMode, + StrictLegacyMode, BlockingMode, } from './ReactTypeOfMode'; import { @@ -357,7 +357,7 @@ function updateForwardRef( ); if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { @@ -889,7 +889,7 @@ function updateFunctionComponent( ); if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { @@ -1068,7 +1068,7 @@ function finishClassComponent( nextChildren = instance.render(); if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { @@ -1478,7 +1478,7 @@ function mountIndeterminateComponent( } } - if (workInProgress.mode & StrictMode) { + if (workInProgress.mode & StrictLegacyMode) { ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, null); } @@ -1615,7 +1615,7 @@ function mountIndeterminateComponent( if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.new.js b/packages/react-reconciler/src/ReactFiberClassComponent.new.js index 389b4877852db..8259e59a70bdd 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.new.js @@ -19,7 +19,7 @@ import { enableDebugTracing, enableSchedulingProfiler, warnAboutDeprecatedLifecycles, - enableDoubleInvokingEffects, + enableStrictEffects, } from 'shared/ReactFeatureFlags'; import ReactStrictModeWarnings from './ReactStrictModeWarnings.new'; import {isMounted} from './ReactFiberTreeReflection'; @@ -31,11 +31,10 @@ import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols'; import {resolveDefaultProps} from './ReactFiberLazyComponent.new'; import { - BlockingMode, - ConcurrentMode, DebugTracingMode, NoMode, - StrictMode, + StrictLegacyMode, + StrictEffectsMode, } from './ReactTypeOfMode'; import { @@ -165,7 +164,7 @@ export function applyDerivedStateFromProps( if (__DEV__) { if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { @@ -318,7 +317,7 @@ function checkShouldComponentUpdate( if (__DEV__) { if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { @@ -655,7 +654,7 @@ function constructClassInstance( if (__DEV__) { if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { @@ -862,7 +861,7 @@ function mountClassInstance( } } - if (workInProgress.mode & StrictMode) { + if (workInProgress.mode & StrictLegacyMode) { ReactStrictModeWarnings.recordLegacyContextWarning( workInProgress, instance, @@ -909,8 +908,8 @@ function mountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && - (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. workInProgress.flags |= MountLayoutDev | Update; @@ -988,8 +987,8 @@ function resumeMountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && - (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. workInProgress.flags |= MountLayoutDev | Update; @@ -1040,8 +1039,8 @@ function resumeMountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && - (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. workInProgress.flags |= MountLayoutDev | Update; @@ -1055,8 +1054,8 @@ function resumeMountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && - (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. workInProgress.flags |= MountLayoutDev | Update; diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.old.js b/packages/react-reconciler/src/ReactFiberClassComponent.old.js index 5f366e7c253a2..fe20322bb15a6 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.old.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.old.js @@ -19,7 +19,7 @@ import { enableDebugTracing, enableSchedulingProfiler, warnAboutDeprecatedLifecycles, - enableDoubleInvokingEffects, + enableStrictEffects, } from 'shared/ReactFeatureFlags'; import ReactStrictModeWarnings from './ReactStrictModeWarnings.old'; import {isMounted} from './ReactFiberTreeReflection'; @@ -31,11 +31,10 @@ import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols'; import {resolveDefaultProps} from './ReactFiberLazyComponent.old'; import { - BlockingMode, - ConcurrentMode, DebugTracingMode, NoMode, - StrictMode, + StrictLegacyMode, + StrictEffectsMode, } from './ReactTypeOfMode'; import { @@ -165,7 +164,7 @@ export function applyDerivedStateFromProps( if (__DEV__) { if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { @@ -318,7 +317,7 @@ function checkShouldComponentUpdate( if (__DEV__) { if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { @@ -655,7 +654,7 @@ function constructClassInstance( if (__DEV__) { if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { @@ -862,7 +861,7 @@ function mountClassInstance( } } - if (workInProgress.mode & StrictMode) { + if (workInProgress.mode & StrictLegacyMode) { ReactStrictModeWarnings.recordLegacyContextWarning( workInProgress, instance, @@ -909,8 +908,8 @@ function mountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && - (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. workInProgress.flags |= MountLayoutDev | Update; @@ -988,8 +987,8 @@ function resumeMountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && - (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. workInProgress.flags |= MountLayoutDev | Update; @@ -1040,8 +1039,8 @@ function resumeMountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && - (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. workInProgress.flags |= MountLayoutDev | Update; @@ -1055,8 +1054,8 @@ function resumeMountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && - (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. workInProgress.flags |= MountLayoutDev | Update; diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index e170114848cc8..33ed1b5c95ce7 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -35,7 +35,7 @@ import { enableSuspenseServerRenderer, enableSuspenseCallback, enableScopeAPI, - enableDoubleInvokingEffects, + enableStrictEffects, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -2475,9 +2475,9 @@ function ensureCorrectReturnPointer(fiber, expectedReturnFiber) { } function invokeLayoutEffectMountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { - // We don't need to re-check for legacy roots here. - // This function will not be called within legacy roots. + if (__DEV__ && enableStrictEffects) { + // We don't need to re-check StrictEffectsMode here. + // This function is only called if that check has already passed. switch (fiber.tag) { case FunctionComponent: case ForwardRef: @@ -2509,9 +2509,9 @@ function invokeLayoutEffectMountInDEV(fiber: Fiber): void { } function invokePassiveEffectMountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { - // We don't need to re-check for legacy roots here. - // This function will not be called within legacy roots. + if (__DEV__ && enableStrictEffects) { + // We don't need to re-check StrictEffectsMode here. + // This function is only called if that check has already passed. switch (fiber.tag) { case FunctionComponent: case ForwardRef: @@ -2534,9 +2534,9 @@ function invokePassiveEffectMountInDEV(fiber: Fiber): void { } function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { - // We don't need to re-check for legacy roots here. - // This function will not be called within legacy roots. + if (__DEV__ && enableStrictEffects) { + // We don't need to re-check StrictEffectsMode here. + // This function is only called if that check has already passed. switch (fiber.tag) { case FunctionComponent: case ForwardRef: @@ -2578,9 +2578,9 @@ function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void { } function invokePassiveEffectUnmountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { - // We don't need to re-check for legacy roots here. - // This function will not be called within legacy roots. + if (__DEV__ && enableStrictEffects) { + // We don't need to re-check StrictEffectsMode here. + // This function is only called if that check has already passed. switch (fiber.tag) { case FunctionComponent: case ForwardRef: diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index 7a27bbc48f875..7574eb4fd3d3c 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -35,7 +35,7 @@ import { enableSuspenseServerRenderer, enableSuspenseCallback, enableScopeAPI, - enableDoubleInvokingEffects, + enableStrictEffects, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -2475,9 +2475,9 @@ function ensureCorrectReturnPointer(fiber, expectedReturnFiber) { } function invokeLayoutEffectMountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { - // We don't need to re-check for legacy roots here. - // This function will not be called within legacy roots. + if (__DEV__ && enableStrictEffects) { + // We don't need to re-check StrictEffectsMode here. + // This function is only called if that check has already passed. switch (fiber.tag) { case FunctionComponent: case ForwardRef: @@ -2509,9 +2509,9 @@ function invokeLayoutEffectMountInDEV(fiber: Fiber): void { } function invokePassiveEffectMountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { - // We don't need to re-check for legacy roots here. - // This function will not be called within legacy roots. + if (__DEV__ && enableStrictEffects) { + // We don't need to re-check StrictEffectsMode here. + // This function is only called if that check has already passed. switch (fiber.tag) { case FunctionComponent: case ForwardRef: @@ -2534,9 +2534,9 @@ function invokePassiveEffectMountInDEV(fiber: Fiber): void { } function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { - // We don't need to re-check for legacy roots here. - // This function will not be called within legacy roots. + if (__DEV__ && enableStrictEffects) { + // We don't need to re-check StrictEffectsMode here. + // This function is only called if that check has already passed. switch (fiber.tag) { case FunctionComponent: case ForwardRef: @@ -2578,9 +2578,9 @@ function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void { } function invokePassiveEffectUnmountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { - // We don't need to re-check for legacy roots here. - // This function will not be called within legacy roots. + if (__DEV__ && enableStrictEffects) { + // We don't need to re-check StrictEffectsMode here. + // This function is only called if that check has already passed. switch (fiber.tag) { case FunctionComponent: case ForwardRef: diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 32144cc1b0505..9d1fa5f82ed8e 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -29,14 +29,14 @@ import { enableCache, decoupleUpdatePriorityFromScheduler, enableUseRefAccessWarning, - enableDoubleInvokingEffects, + enableStrictEffects, } from 'shared/ReactFeatureFlags'; import { NoMode, BlockingMode, - ConcurrentMode, DebugTracingMode, + StrictEffectsMode, } from './ReactTypeOfMode'; import { NoLane, @@ -509,8 +509,8 @@ export function bailoutHooks( // complete phase (bubbleProperties). if ( __DEV__ && - enableDoubleInvokingEffects && - (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (workInProgress.mode & StrictEffectsMode) !== NoMode ) { workInProgress.flags &= ~( MountPassiveDevEffect | @@ -1423,8 +1423,8 @@ function mountEffect( } if ( __DEV__ && - enableDoubleInvokingEffects && - (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode ) { return mountEffectImpl( MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect, @@ -1461,8 +1461,8 @@ function mountLayoutEffect( ): void { if ( __DEV__ && - enableDoubleInvokingEffects && - (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode ) { return mountEffectImpl( MountLayoutDevEffect | UpdateEffect, @@ -1533,8 +1533,8 @@ function mountImperativeHandle( if ( __DEV__ && - enableDoubleInvokingEffects && - (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode ) { return mountEffectImpl( MountLayoutDevEffect | UpdateEffect, @@ -1830,7 +1830,11 @@ function mountOpaqueIdentifier(): OpaqueIDType | void { const setId = mountState(id)[1]; if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) { - if (__DEV__ && enableDoubleInvokingEffects) { + if ( + __DEV__ && + enableStrictEffects && + (currentlyRenderingFiber.mode & StrictEffectsMode) === NoMode + ) { currentlyRenderingFiber.flags |= MountPassiveDevEffect | PassiveEffect; } else { currentlyRenderingFiber.flags |= PassiveEffect; diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index ce0ac36636562..61f1a17452e4b 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -29,14 +29,14 @@ import { enableCache, decoupleUpdatePriorityFromScheduler, enableUseRefAccessWarning, - enableDoubleInvokingEffects, + enableStrictEffects, } from 'shared/ReactFeatureFlags'; import { NoMode, BlockingMode, - ConcurrentMode, DebugTracingMode, + StrictEffectsMode, } from './ReactTypeOfMode'; import { NoLane, @@ -509,8 +509,8 @@ export function bailoutHooks( // complete phase (bubbleProperties). if ( __DEV__ && - enableDoubleInvokingEffects && - (workInProgress.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (workInProgress.mode & StrictEffectsMode) !== NoMode ) { workInProgress.flags &= ~( MountPassiveDevEffect | @@ -1423,8 +1423,8 @@ function mountEffect( } if ( __DEV__ && - enableDoubleInvokingEffects && - (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode ) { return mountEffectImpl( MountPassiveDevEffect | PassiveEffect | PassiveStaticEffect, @@ -1461,8 +1461,8 @@ function mountLayoutEffect( ): void { if ( __DEV__ && - enableDoubleInvokingEffects && - (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode ) { return mountEffectImpl( MountLayoutDevEffect | UpdateEffect, @@ -1533,8 +1533,8 @@ function mountImperativeHandle( if ( __DEV__ && - enableDoubleInvokingEffects && - (currentlyRenderingFiber.mode & (BlockingMode | ConcurrentMode)) !== NoMode + enableStrictEffects && + (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode ) { return mountEffectImpl( MountLayoutDevEffect | UpdateEffect, @@ -1830,7 +1830,11 @@ function mountOpaqueIdentifier(): OpaqueIDType | void { const setId = mountState(id)[1]; if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) { - if (__DEV__ && enableDoubleInvokingEffects) { + if ( + __DEV__ && + enableStrictEffects && + (currentlyRenderingFiber.mode & StrictEffectsMode) === NoMode + ) { currentlyRenderingFiber.flags |= MountPassiveDevEffect | PassiveEffect; } else { currentlyRenderingFiber.flags |= PassiveEffect; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index be5ee15970dc7..cc639fa0865f6 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -75,7 +75,7 @@ import { resetCurrentFiber as resetCurrentDebugFiberInDEV, setCurrentFiber as setCurrentDebugFiberInDEV, } from './ReactCurrentFiber'; -import {StrictMode} from './ReactTypeOfMode'; +import {StrictLegacyMode} from './ReactTypeOfMode'; import { SyncLane, InputDiscreteHydrationLane, @@ -204,7 +204,7 @@ function findHostInstanceWithWarning( if (hostFiber === null) { return null; } - if (hostFiber.mode & StrictMode) { + if (hostFiber.mode & StrictLegacyMode) { const componentName = getComponentName(fiber.type) || 'Component'; if (!didWarnAboutFindNodeInStrictMode[componentName]) { didWarnAboutFindNodeInStrictMode[componentName] = true; @@ -212,7 +212,7 @@ function findHostInstanceWithWarning( const previousFiber = ReactCurrentFiberCurrent; try { setCurrentDebugFiberInDEV(hostFiber); - if (fiber.mode & StrictMode) { + if (fiber.mode & StrictLegacyMode) { console.error( '%s is deprecated in StrictMode. ' + '%s was passed an instance of %s which is inside StrictMode. ' + @@ -256,8 +256,15 @@ export function createContainer( tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, + strictModeLevelOverride: null | number, ): OpaqueRoot { - return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks); + return createFiberRoot( + containerInfo, + tag, + hydrate, + hydrationCallbacks, + strictModeLevelOverride, + ); } export function updateContainer( diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index 77032eb24b108..3044bf934daf8 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -75,7 +75,7 @@ import { resetCurrentFiber as resetCurrentDebugFiberInDEV, setCurrentFiber as setCurrentDebugFiberInDEV, } from './ReactCurrentFiber'; -import {StrictMode} from './ReactTypeOfMode'; +import {StrictLegacyMode} from './ReactTypeOfMode'; import { SyncLane, InputDiscreteHydrationLane, @@ -204,7 +204,7 @@ function findHostInstanceWithWarning( if (hostFiber === null) { return null; } - if (hostFiber.mode & StrictMode) { + if (hostFiber.mode & StrictLegacyMode) { const componentName = getComponentName(fiber.type) || 'Component'; if (!didWarnAboutFindNodeInStrictMode[componentName]) { didWarnAboutFindNodeInStrictMode[componentName] = true; @@ -212,7 +212,7 @@ function findHostInstanceWithWarning( const previousFiber = ReactCurrentFiberCurrent; try { setCurrentDebugFiberInDEV(hostFiber); - if (fiber.mode & StrictMode) { + if (fiber.mode & StrictLegacyMode) { console.error( '%s is deprecated in StrictMode. ' + '%s was passed an instance of %s which is inside StrictMode. ' + @@ -256,8 +256,15 @@ export function createContainer( tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, + strictModeLevelOverride: null | number, ): OpaqueRoot { - return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks); + return createFiberRoot( + containerInfo, + tag, + hydrate, + hydrationCallbacks, + strictModeLevelOverride, + ); } export function updateContainer( diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js index 9057137ec61a8..ae6a5bdb596f6 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.new.js +++ b/packages/react-reconciler/src/ReactFiberRoot.new.js @@ -91,6 +91,7 @@ export function createFiberRoot( tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, + strictModeLevelOverride: null | number, ): FiberRoot { const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any); if (enableSuspenseCallback) { @@ -99,7 +100,7 @@ export function createFiberRoot( // Cyclic construction. This cheats the type system right now because // stateNode is any. - const uninitializedFiber = createHostRootFiber(tag); + const uninitializedFiber = createHostRootFiber(tag, strictModeLevelOverride); root.current = uninitializedFiber; uninitializedFiber.stateNode = root; diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js index 92ec811dd5589..0c0d45c098720 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.old.js +++ b/packages/react-reconciler/src/ReactFiberRoot.old.js @@ -91,6 +91,7 @@ export function createFiberRoot( tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, + strictModeLevelOverride: null | number, ): FiberRoot { const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any); if (enableSuspenseCallback) { @@ -99,7 +100,7 @@ export function createFiberRoot( // Cyclic construction. This cheats the type system right now because // stateNode is any. - const uninitializedFiber = createHostRootFiber(tag); + const uninitializedFiber = createHostRootFiber(tag, strictModeLevelOverride); root.current = uninitializedFiber; uninitializedFiber.stateNode = root; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 918261a2e3562..7caee07872517 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -32,7 +32,7 @@ import { enableDebugTracing, enableSchedulingProfiler, disableSchedulerTimeoutInWorkLoop, - enableDoubleInvokingEffects, + enableStrictEffects, skipUnmountedBoundaries, enableNativeEventPriorityInference, } from 'shared/ReactFeatureFlags'; @@ -104,7 +104,7 @@ import { } from './ReactFiber.new'; import { NoMode, - StrictMode, + StrictLegacyMode, ProfileMode, BlockingMode, ConcurrentMode, @@ -2070,7 +2070,7 @@ function commitRootImpl(root, renderPriorityLevel) { legacyErrorBoundariesThatAlreadyFailed = null; } - if (__DEV__ && enableDoubleInvokingEffects) { + if (__DEV__ && enableStrictEffects) { if (!rootDidHavePassiveEffects) { commitDoubleInvokeEffectsInDEV(root.current, false); } @@ -2257,7 +2257,7 @@ function flushPassiveEffectsImpl() { markPassiveEffectsStopped(); } - if (__DEV__ && enableDoubleInvokingEffects) { + if (__DEV__ && enableStrictEffects) { commitDoubleInvokeEffectsInDEV(root.current, true); } @@ -2560,11 +2560,10 @@ function commitDoubleInvokeEffectsInDEV( fiber: Fiber, hasPassiveEffects: boolean, ) { - if (__DEV__ && enableDoubleInvokingEffects) { - // Never double-invoke effects for legacy roots. - if ((fiber.mode & (BlockingMode | ConcurrentMode)) === NoMode) { - return; - } + if (__DEV__ && enableStrictEffects) { + // TODO (StrictEffects) Should we set a marker on the root if it contains strict effects + // so we don't traverse unnecessarily? similar to subtreeFlags but just at the root level. + // Maybe not a big deal since this is DEV only behavior. setCurrentDebugFiberInDEV(fiber); invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectUnmountInDEV); @@ -2589,9 +2588,9 @@ function invokeEffectsInDev( fiberFlags: Flags, invokeEffectFn: (fiber: Fiber) => void, ): void { - if (__DEV__ && enableDoubleInvokingEffects) { - // We don't need to re-check for legacy roots here. - // This function will not be called within legacy roots. + if (__DEV__ && enableStrictEffects) { + // We don't need to re-check StrictEffectsMode here. + // This function is only called if that check has already passed. let current = firstChild; let subtreeRoot = null; @@ -2934,7 +2933,7 @@ export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void { if (__DEV__) { if ( warnsIfNotActing === true && - (fiber.mode & StrictMode) !== NoMode && + (fiber.mode & StrictLegacyMode) !== NoMode && IsSomeRendererActing.current === false && IsThisRendererActing.current === false ) { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 12ef384a47527..1467929ce4597 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -32,7 +32,7 @@ import { enableDebugTracing, enableSchedulingProfiler, disableSchedulerTimeoutInWorkLoop, - enableDoubleInvokingEffects, + enableStrictEffects, skipUnmountedBoundaries, enableNativeEventPriorityInference, } from 'shared/ReactFeatureFlags'; @@ -104,7 +104,7 @@ import { } from './ReactFiber.old'; import { NoMode, - StrictMode, + StrictLegacyMode, ProfileMode, BlockingMode, ConcurrentMode, @@ -2070,7 +2070,7 @@ function commitRootImpl(root, renderPriorityLevel) { legacyErrorBoundariesThatAlreadyFailed = null; } - if (__DEV__ && enableDoubleInvokingEffects) { + if (__DEV__ && enableStrictEffects) { if (!rootDidHavePassiveEffects) { commitDoubleInvokeEffectsInDEV(root.current, false); } @@ -2257,7 +2257,7 @@ function flushPassiveEffectsImpl() { markPassiveEffectsStopped(); } - if (__DEV__ && enableDoubleInvokingEffects) { + if (__DEV__ && enableStrictEffects) { commitDoubleInvokeEffectsInDEV(root.current, true); } @@ -2560,11 +2560,10 @@ function commitDoubleInvokeEffectsInDEV( fiber: Fiber, hasPassiveEffects: boolean, ) { - if (__DEV__ && enableDoubleInvokingEffects) { - // Never double-invoke effects for legacy roots. - if ((fiber.mode & (BlockingMode | ConcurrentMode)) === NoMode) { - return; - } + if (__DEV__ && enableStrictEffects) { + // TODO (StrictEffects) Should we set a marker on the root if it contains strict effects + // so we don't traverse unnecessarily? similar to subtreeFlags but just at the root level. + // Maybe not a big deal since this is DEV only behavior. setCurrentDebugFiberInDEV(fiber); invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectUnmountInDEV); @@ -2589,9 +2588,9 @@ function invokeEffectsInDev( fiberFlags: Flags, invokeEffectFn: (fiber: Fiber) => void, ): void { - if (__DEV__ && enableDoubleInvokingEffects) { - // We don't need to re-check for legacy roots here. - // This function will not be called within legacy roots. + if (__DEV__ && enableStrictEffects) { + // We don't need to re-check StrictEffectsMode here. + // This function is only called if that check has already passed. let current = firstChild; let subtreeRoot = null; @@ -2934,7 +2933,7 @@ export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void { if (__DEV__) { if ( warnsIfNotActing === true && - (fiber.mode & StrictMode) !== NoMode && + (fiber.mode & StrictLegacyMode) !== NoMode && IsSomeRendererActing.current === false && IsThisRendererActing.current === false ) { diff --git a/packages/react-reconciler/src/ReactStrictModeWarnings.new.js b/packages/react-reconciler/src/ReactStrictModeWarnings.new.js index 5dd09a8cc80da..972c106c632cf 100644 --- a/packages/react-reconciler/src/ReactStrictModeWarnings.new.js +++ b/packages/react-reconciler/src/ReactStrictModeWarnings.new.js @@ -14,7 +14,7 @@ import { setCurrentFiber as setCurrentDebugFiberInDEV, } from './ReactCurrentFiber'; import getComponentName from 'shared/getComponentName'; -import {StrictMode} from './ReactTypeOfMode'; +import {StrictLegacyMode} from './ReactTypeOfMode'; type FiberArray = Array; type FiberToFiberComponentsMap = Map; @@ -33,7 +33,7 @@ if (__DEV__) { let node = fiber; while (node !== null) { - if (node.mode & StrictMode) { + if (node.mode & StrictLegacyMode) { maybeStrictRoot = node; } node = node.return; @@ -78,7 +78,7 @@ if (__DEV__) { } if ( - fiber.mode & StrictMode && + fiber.mode & StrictLegacyMode && typeof instance.UNSAFE_componentWillMount === 'function' ) { pendingUNSAFE_ComponentWillMountWarnings.push(fiber); @@ -92,7 +92,7 @@ if (__DEV__) { } if ( - fiber.mode & StrictMode && + fiber.mode & StrictLegacyMode && typeof instance.UNSAFE_componentWillReceiveProps === 'function' ) { pendingUNSAFE_ComponentWillReceivePropsWarnings.push(fiber); @@ -106,7 +106,7 @@ if (__DEV__) { } if ( - fiber.mode & StrictMode && + fiber.mode & StrictLegacyMode && typeof instance.UNSAFE_componentWillUpdate === 'function' ) { pendingUNSAFE_ComponentWillUpdateWarnings.push(fiber); diff --git a/packages/react-reconciler/src/ReactStrictModeWarnings.old.js b/packages/react-reconciler/src/ReactStrictModeWarnings.old.js index 5dd09a8cc80da..972c106c632cf 100644 --- a/packages/react-reconciler/src/ReactStrictModeWarnings.old.js +++ b/packages/react-reconciler/src/ReactStrictModeWarnings.old.js @@ -14,7 +14,7 @@ import { setCurrentFiber as setCurrentDebugFiberInDEV, } from './ReactCurrentFiber'; import getComponentName from 'shared/getComponentName'; -import {StrictMode} from './ReactTypeOfMode'; +import {StrictLegacyMode} from './ReactTypeOfMode'; type FiberArray = Array; type FiberToFiberComponentsMap = Map; @@ -33,7 +33,7 @@ if (__DEV__) { let node = fiber; while (node !== null) { - if (node.mode & StrictMode) { + if (node.mode & StrictLegacyMode) { maybeStrictRoot = node; } node = node.return; @@ -78,7 +78,7 @@ if (__DEV__) { } if ( - fiber.mode & StrictMode && + fiber.mode & StrictLegacyMode && typeof instance.UNSAFE_componentWillMount === 'function' ) { pendingUNSAFE_ComponentWillMountWarnings.push(fiber); @@ -92,7 +92,7 @@ if (__DEV__) { } if ( - fiber.mode & StrictMode && + fiber.mode & StrictLegacyMode && typeof instance.UNSAFE_componentWillReceiveProps === 'function' ) { pendingUNSAFE_ComponentWillReceivePropsWarnings.push(fiber); @@ -106,7 +106,7 @@ if (__DEV__) { } if ( - fiber.mode & StrictMode && + fiber.mode & StrictLegacyMode && typeof instance.UNSAFE_componentWillUpdate === 'function' ) { pendingUNSAFE_ComponentWillUpdateWarnings.push(fiber); diff --git a/packages/react-reconciler/src/ReactTypeOfMode.js b/packages/react-reconciler/src/ReactTypeOfMode.js index f6092058d2ba0..a6499be7aca11 100644 --- a/packages/react-reconciler/src/ReactTypeOfMode.js +++ b/packages/react-reconciler/src/ReactTypeOfMode.js @@ -9,11 +9,11 @@ export type TypeOfMode = number; -export const NoMode = 0b00000; -export const StrictMode = 0b00001; -// TODO: Remove BlockingMode and ConcurrentMode by reading from the root -// tag instead -export const BlockingMode = 0b00010; -export const ConcurrentMode = 0b00100; -export const ProfileMode = 0b01000; -export const DebugTracingMode = 0b10000; +export const NoMode = /* */ 0b000000; +// TODO: Remove BlockingMode and ConcurrentMode by reading from the root tag instead +export const BlockingMode = /* */ 0b000001; +export const ConcurrentMode = /* */ 0b000010; +export const ProfileMode = /* */ 0b000100; +export const DebugTracingMode = /* */ 0b001000; +export const StrictLegacyMode = /* */ 0b010000; +export const StrictEffectsMode = /* */ 0b100000; diff --git a/packages/react-reconciler/src/ReactUpdateQueue.new.js b/packages/react-reconciler/src/ReactUpdateQueue.new.js index bb4e9e1bc52dc..0bdcbf580764a 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.new.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.new.js @@ -104,7 +104,7 @@ import {Callback, ShouldCapture, DidCapture} from './ReactFiberFlags'; import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags'; -import {StrictMode} from './ReactTypeOfMode'; +import {StrictLegacyMode} from './ReactTypeOfMode'; import { markSkippedUpdateLanes, isInterleavedUpdate, @@ -392,7 +392,7 @@ function getStateFromUpdate( if (__DEV__) { if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { @@ -425,7 +425,7 @@ function getStateFromUpdate( if (__DEV__) { if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { diff --git a/packages/react-reconciler/src/ReactUpdateQueue.old.js b/packages/react-reconciler/src/ReactUpdateQueue.old.js index 6b578ef9c80d6..209abfb32e743 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.old.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.old.js @@ -104,7 +104,7 @@ import {Callback, ShouldCapture, DidCapture} from './ReactFiberFlags'; import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags'; -import {StrictMode} from './ReactTypeOfMode'; +import {StrictLegacyMode} from './ReactTypeOfMode'; import { markSkippedUpdateLanes, isInterleavedUpdate, @@ -392,7 +392,7 @@ function getStateFromUpdate( if (__DEV__) { if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { @@ -425,7 +425,7 @@ function getStateFromUpdate( if (__DEV__) { if ( debugRenderPhaseSideEffectsForStrictMode && - workInProgress.mode & StrictMode + workInProgress.mode & StrictLegacyMode ) { disableLogs(); try { diff --git a/packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.internal.js b/packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.internal.js deleted file mode 100644 index b53b7b719c6ba..0000000000000 --- a/packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.internal.js +++ /dev/null @@ -1,747 +0,0 @@ -/** - * 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 - */ - -'use strict'; - -let React; -let ReactFeatureFlags; -let ReactNoop; -let Scheduler; - -function shouldDoubleInvokingEffects() { - // For now, this feature only exists in the old fork (while the new fork is being bisected). - // Eventually we'll land it in both forks. - return __DEV__; -} - -describe('ReactDoubleInvokeEvents', () => { - beforeEach(() => { - jest.resetModules(); - React = require('react'); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactNoop = require('react-noop-renderer'); - Scheduler = require('scheduler'); - - ReactFeatureFlags.enableDoubleInvokingEffects = shouldDoubleInvokingEffects(); - }); - - it('should not double invoke effects in legacy mode', () => { - function App({text}) { - React.useEffect(() => { - Scheduler.unstable_yieldValue('useEffect mount'); - return () => Scheduler.unstable_yieldValue('useEffect unmount'); - }); - - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('useLayoutEffect mount'); - return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount'); - }); - - return text; - } - - ReactNoop.act(() => { - ReactNoop.renderLegacySyncRoot(); - }); - - expect(Scheduler).toHaveYielded([ - 'useLayoutEffect mount', - 'useEffect mount', - ]); - }); - - it('should not double invoke class lifecycles in legacy mode', () => { - class App extends React.PureComponent { - componentDidMount() { - Scheduler.unstable_yieldValue('componentDidMount'); - } - - componentDidUpdate() { - Scheduler.unstable_yieldValue('componentDidUpdate'); - } - - componentWillUnmount() { - Scheduler.unstable_yieldValue('componentWillUnmount'); - } - - render() { - return this.props.text; - } - } - - ReactNoop.act(() => { - ReactNoop.renderLegacySyncRoot(); - }); - - expect(Scheduler).toHaveYielded(['componentDidMount']); - }); - - it('should flush double-invoked effects within the same frame as layout effects if there are no passive effects', () => { - function ComponentWithEffects({label}) { - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue(`useLayoutEffect mount "${label}"`); - return () => - Scheduler.unstable_yieldValue(`useLayoutEffect unmount "${label}"`); - }); - - return label; - } - - ReactNoop.act(() => { - ReactNoop.render( - <> - - , - ); - - if (shouldDoubleInvokingEffects()) { - expect(Scheduler).toFlushUntilNextPaint([ - 'useLayoutEffect mount "one"', - 'useLayoutEffect unmount "one"', - 'useLayoutEffect mount "one"', - ]); - } else { - expect(Scheduler).toFlushUntilNextPaint([ - 'useLayoutEffect mount "one"', - ]); - } - }); - - ReactNoop.act(() => { - ReactNoop.render( - <> - - - , - ); - - if (shouldDoubleInvokingEffects()) { - expect(Scheduler).toFlushUntilNextPaint([ - // Cleanup and re-run "one" (and "two") since there is no dependencies array. - 'useLayoutEffect unmount "one"', - 'useLayoutEffect mount "one"', - 'useLayoutEffect mount "two"', - - // Since "two" is new, it should be double-invoked. - 'useLayoutEffect unmount "two"', - 'useLayoutEffect mount "two"', - ]); - } else { - expect(Scheduler).toFlushUntilNextPaint([ - 'useLayoutEffect unmount "one"', - 'useLayoutEffect mount "one"', - 'useLayoutEffect mount "two"', - ]); - } - }); - }); - - // This test also verifies that double-invoked effects flush synchronously - // within the same frame as passive effects. - it('should double invoke effects only for newly mounted components', () => { - function ComponentWithEffects({label}) { - React.useEffect(() => { - Scheduler.unstable_yieldValue(`useEffect mount "${label}"`); - return () => - Scheduler.unstable_yieldValue(`useEffect unmount "${label}"`); - }); - - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue(`useLayoutEffect mount "${label}"`); - return () => - Scheduler.unstable_yieldValue(`useLayoutEffect unmount "${label}"`); - }); - - return label; - } - - ReactNoop.act(() => { - ReactNoop.render( - <> - - , - ); - - if (shouldDoubleInvokingEffects()) { - expect(Scheduler).toFlushAndYieldThrough([ - 'useLayoutEffect mount "one"', - ]); - expect(Scheduler).toFlushAndYield([ - 'useEffect mount "one"', - 'useLayoutEffect unmount "one"', - 'useEffect unmount "one"', - 'useLayoutEffect mount "one"', - 'useEffect mount "one"', - ]); - } else { - expect(Scheduler).toFlushAndYieldThrough([ - 'useLayoutEffect mount "one"', - ]); - expect(Scheduler).toFlushAndYield(['useEffect mount "one"']); - } - }); - - ReactNoop.act(() => { - ReactNoop.render( - <> - - - , - ); - - if (shouldDoubleInvokingEffects()) { - expect(Scheduler).toFlushAndYieldThrough([ - // Cleanup and re-run "one" (and "two") since there is no dependencies array. - 'useLayoutEffect unmount "one"', - 'useLayoutEffect mount "one"', - 'useLayoutEffect mount "two"', - ]); - expect(Scheduler).toFlushAndYield([ - 'useEffect unmount "one"', - 'useEffect mount "one"', - 'useEffect mount "two"', - - // Since "two" is new, it should be double-invoked. - 'useLayoutEffect unmount "two"', - 'useEffect unmount "two"', - 'useLayoutEffect mount "two"', - 'useEffect mount "two"', - ]); - } else { - expect(Scheduler).toFlushAndYieldThrough([ - 'useLayoutEffect unmount "one"', - 'useLayoutEffect mount "one"', - 'useLayoutEffect mount "two"', - ]); - expect(Scheduler).toFlushAndYield([ - 'useEffect unmount "one"', - 'useEffect mount "one"', - 'useEffect mount "two"', - ]); - } - }); - }); - - it('double invoking for effects for modern roots', () => { - function App({text}) { - React.useEffect(() => { - Scheduler.unstable_yieldValue('useEffect mount'); - return () => Scheduler.unstable_yieldValue('useEffect unmount'); - }); - - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('useLayoutEffect mount'); - return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount'); - }); - - return text; - } - ReactNoop.act(() => { - ReactNoop.render(); - }); - - if (shouldDoubleInvokingEffects()) { - expect(Scheduler).toHaveYielded([ - 'useLayoutEffect mount', - 'useEffect mount', - 'useLayoutEffect unmount', - 'useEffect unmount', - 'useLayoutEffect mount', - 'useEffect mount', - ]); - } else { - expect(Scheduler).toHaveYielded([ - 'useLayoutEffect mount', - 'useEffect mount', - ]); - } - - ReactNoop.act(() => { - ReactNoop.render(); - }); - - expect(Scheduler).toHaveYielded([ - 'useLayoutEffect unmount', - 'useLayoutEffect mount', - 'useEffect unmount', - 'useEffect mount', - ]); - - ReactNoop.act(() => { - ReactNoop.render(null); - }); - - expect(Scheduler).toHaveYielded([ - 'useLayoutEffect unmount', - 'useEffect unmount', - ]); - }); - - it('multiple effects are double invoked in the right order (all mounted, all unmounted, all remounted)', () => { - function App({text}) { - React.useEffect(() => { - Scheduler.unstable_yieldValue('useEffect One mount'); - return () => Scheduler.unstable_yieldValue('useEffect One unmount'); - }); - - React.useEffect(() => { - Scheduler.unstable_yieldValue('useEffect Two mount'); - return () => Scheduler.unstable_yieldValue('useEffect Two unmount'); - }); - - return text; - } - - ReactNoop.act(() => { - ReactNoop.render(); - }); - - if (shouldDoubleInvokingEffects()) { - expect(Scheduler).toHaveYielded([ - 'useEffect One mount', - 'useEffect Two mount', - 'useEffect One unmount', - 'useEffect Two unmount', - 'useEffect One mount', - 'useEffect Two mount', - ]); - } else { - expect(Scheduler).toHaveYielded([ - 'useEffect One mount', - 'useEffect Two mount', - ]); - } - - ReactNoop.act(() => { - ReactNoop.render(); - }); - - expect(Scheduler).toHaveYielded([ - 'useEffect One unmount', - 'useEffect Two unmount', - 'useEffect One mount', - 'useEffect Two mount', - ]); - - ReactNoop.act(() => { - ReactNoop.render(null); - }); - - expect(Scheduler).toHaveYielded([ - 'useEffect One unmount', - 'useEffect Two unmount', - ]); - }); - - it('multiple layout effects are double invoked in the right order (all mounted, all unmounted, all remounted)', () => { - function App({text}) { - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('useLayoutEffect One mount'); - return () => - Scheduler.unstable_yieldValue('useLayoutEffect One unmount'); - }); - - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('useLayoutEffect Two mount'); - return () => - Scheduler.unstable_yieldValue('useLayoutEffect Two unmount'); - }); - - return text; - } - - ReactNoop.act(() => { - ReactNoop.render(); - }); - - if (shouldDoubleInvokingEffects()) { - expect(Scheduler).toHaveYielded([ - 'useLayoutEffect One mount', - 'useLayoutEffect Two mount', - 'useLayoutEffect One unmount', - 'useLayoutEffect Two unmount', - 'useLayoutEffect One mount', - 'useLayoutEffect Two mount', - ]); - } else { - expect(Scheduler).toHaveYielded([ - 'useLayoutEffect One mount', - 'useLayoutEffect Two mount', - ]); - } - - ReactNoop.act(() => { - ReactNoop.render(); - }); - - expect(Scheduler).toHaveYielded([ - 'useLayoutEffect One unmount', - 'useLayoutEffect Two unmount', - 'useLayoutEffect One mount', - 'useLayoutEffect Two mount', - ]); - - ReactNoop.act(() => { - ReactNoop.render(null); - }); - - expect(Scheduler).toHaveYielded([ - 'useLayoutEffect One unmount', - 'useLayoutEffect Two unmount', - ]); - }); - - it('useEffect and useLayoutEffect is called twice when there is no unmount', () => { - function App({text}) { - React.useEffect(() => { - Scheduler.unstable_yieldValue('useEffect mount'); - }); - - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('useLayoutEffect mount'); - }); - - return text; - } - - ReactNoop.act(() => { - ReactNoop.render(); - }); - - if (shouldDoubleInvokingEffects()) { - expect(Scheduler).toHaveYielded([ - 'useLayoutEffect mount', - 'useEffect mount', - 'useLayoutEffect mount', - 'useEffect mount', - ]); - } else { - expect(Scheduler).toHaveYielded([ - 'useLayoutEffect mount', - 'useEffect mount', - ]); - } - - ReactNoop.act(() => { - ReactNoop.render(); - }); - - expect(Scheduler).toHaveYielded([ - 'useLayoutEffect mount', - 'useEffect mount', - ]); - - ReactNoop.act(() => { - ReactNoop.render(null); - }); - - expect(Scheduler).toHaveYielded([]); - }); - - it('passes the right context to class component lifecycles', () => { - class App extends React.PureComponent { - test() {} - - componentDidMount() { - this.test(); - Scheduler.unstable_yieldValue('componentDidMount'); - } - - componentDidUpdate() { - this.test(); - Scheduler.unstable_yieldValue('componentDidUpdate'); - } - - componentWillUnmount() { - this.test(); - Scheduler.unstable_yieldValue('componentWillUnmount'); - } - - render() { - return null; - } - } - - ReactNoop.act(() => { - ReactNoop.render(); - }); - - if (shouldDoubleInvokingEffects()) { - expect(Scheduler).toHaveYielded([ - 'componentDidMount', - 'componentWillUnmount', - 'componentDidMount', - ]); - } else { - expect(Scheduler).toHaveYielded(['componentDidMount']); - } - }); - - it('double invoking works for class components', () => { - class App extends React.PureComponent { - componentDidMount() { - Scheduler.unstable_yieldValue('componentDidMount'); - } - - componentDidUpdate() { - Scheduler.unstable_yieldValue('componentDidUpdate'); - } - - componentWillUnmount() { - Scheduler.unstable_yieldValue('componentWillUnmount'); - } - - render() { - return this.props.text; - } - } - - ReactNoop.act(() => { - ReactNoop.render(); - }); - - if (shouldDoubleInvokingEffects()) { - expect(Scheduler).toHaveYielded([ - 'componentDidMount', - 'componentWillUnmount', - 'componentDidMount', - ]); - } else { - expect(Scheduler).toHaveYielded(['componentDidMount']); - } - - ReactNoop.act(() => { - ReactNoop.render(); - }); - - expect(Scheduler).toHaveYielded(['componentDidUpdate']); - - ReactNoop.act(() => { - ReactNoop.render(null); - }); - - expect(Scheduler).toHaveYielded(['componentWillUnmount']); - }); - - it('double flushing passive effects only results in one double invoke', () => { - function App({text}) { - const [state, setState] = React.useState(0); - React.useEffect(() => { - if (state !== 1) { - setState(1); - } - Scheduler.unstable_yieldValue('useEffect mount'); - return () => Scheduler.unstable_yieldValue('useEffect unmount'); - }); - - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('useLayoutEffect mount'); - return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount'); - }); - - Scheduler.unstable_yieldValue(text); - return text; - } - - ReactNoop.act(() => { - ReactNoop.render(); - }); - - if (shouldDoubleInvokingEffects()) { - expect(Scheduler).toHaveYielded([ - 'mount', - 'useLayoutEffect mount', - 'useEffect mount', - 'useLayoutEffect unmount', - 'useEffect unmount', - 'useLayoutEffect mount', - 'useEffect mount', - 'mount', - 'useLayoutEffect unmount', - 'useLayoutEffect mount', - 'useEffect unmount', - 'useEffect mount', - ]); - } else { - expect(Scheduler).toHaveYielded([ - 'mount', - 'useLayoutEffect mount', - 'useEffect mount', - 'mount', - 'useLayoutEffect unmount', - 'useLayoutEffect mount', - 'useEffect unmount', - 'useEffect mount', - ]); - } - }); - - it('newly mounted components after initial mount get double invoked', () => { - let _setShowChild; - function Child() { - React.useEffect(() => { - Scheduler.unstable_yieldValue('Child useEffect mount'); - return () => Scheduler.unstable_yieldValue('Child useEffect unmount'); - }); - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('Child useLayoutEffect mount'); - return () => - Scheduler.unstable_yieldValue('Child useLayoutEffect unmount'); - }); - - return null; - } - - function App() { - const [showChild, setShowChild] = React.useState(false); - _setShowChild = setShowChild; - React.useEffect(() => { - Scheduler.unstable_yieldValue('App useEffect mount'); - return () => Scheduler.unstable_yieldValue('App useEffect unmount'); - }); - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('App useLayoutEffect mount'); - return () => - Scheduler.unstable_yieldValue('App useLayoutEffect unmount'); - }); - - return showChild && ; - } - - ReactNoop.act(() => { - ReactNoop.render(); - }); - - if (shouldDoubleInvokingEffects()) { - expect(Scheduler).toHaveYielded([ - 'App useLayoutEffect mount', - 'App useEffect mount', - 'App useLayoutEffect unmount', - 'App useEffect unmount', - 'App useLayoutEffect mount', - 'App useEffect mount', - ]); - } else { - expect(Scheduler).toHaveYielded([ - 'App useLayoutEffect mount', - 'App useEffect mount', - ]); - } - - ReactNoop.act(() => { - _setShowChild(true); - }); - - if (shouldDoubleInvokingEffects()) { - expect(Scheduler).toHaveYielded([ - 'App useLayoutEffect unmount', - 'Child useLayoutEffect mount', - 'App useLayoutEffect mount', - 'App useEffect unmount', - 'Child useEffect mount', - 'App useEffect mount', - 'Child useLayoutEffect unmount', - 'Child useEffect unmount', - 'Child useLayoutEffect mount', - 'Child useEffect mount', - ]); - } else { - expect(Scheduler).toHaveYielded([ - 'App useLayoutEffect unmount', - 'Child useLayoutEffect mount', - 'App useLayoutEffect mount', - 'App useEffect unmount', - 'Child useEffect mount', - 'App useEffect mount', - ]); - } - }); - - it('classes and functions are double invoked together correctly', () => { - class ClassChild extends React.PureComponent { - componentDidMount() { - Scheduler.unstable_yieldValue('componentDidMount'); - } - - componentWillUnmount() { - Scheduler.unstable_yieldValue('componentWillUnmount'); - } - - render() { - return this.props.text; - } - } - - function FunctionChild({text}) { - React.useEffect(() => { - Scheduler.unstable_yieldValue('useEffect mount'); - return () => Scheduler.unstable_yieldValue('useEffect unmount'); - }); - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('useLayoutEffect mount'); - return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount'); - }); - return text; - } - - function App({text}) { - return ( - <> - - - - ); - } - - ReactNoop.act(() => { - ReactNoop.render(); - }); - - if (shouldDoubleInvokingEffects()) { - expect(Scheduler).toHaveYielded([ - 'componentDidMount', - 'useLayoutEffect mount', - 'useEffect mount', - 'componentWillUnmount', - 'useLayoutEffect unmount', - 'useEffect unmount', - 'componentDidMount', - 'useLayoutEffect mount', - 'useEffect mount', - ]); - } else { - expect(Scheduler).toHaveYielded([ - 'componentDidMount', - 'useLayoutEffect mount', - 'useEffect mount', - ]); - } - - ReactNoop.act(() => { - ReactNoop.render(); - }); - - expect(Scheduler).toHaveYielded([ - 'useLayoutEffect unmount', - 'useLayoutEffect mount', - 'useEffect unmount', - 'useEffect mount', - ]); - - ReactNoop.act(() => { - ReactNoop.render(null); - }); - - expect(Scheduler).toHaveYielded([ - 'componentWillUnmount', - 'useLayoutEffect unmount', - 'useEffect unmount', - ]); - }); -}); diff --git a/packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.js b/packages/react-reconciler/src/__tests__/StrictEffectsMode-test.js similarity index 99% rename from packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.js rename to packages/react-reconciler/src/__tests__/StrictEffectsMode-test.js index 5b6abbc4f6593..13f7a9ac543c2 100644 --- a/packages/react-reconciler/src/__tests__/ReactDoubleInvokeEvents-test.js +++ b/packages/react-reconciler/src/__tests__/StrictEffectsMode-test.js @@ -14,7 +14,7 @@ let ReactTestRenderer; let Scheduler; let act; -describe('ReactDoubleInvokeEvents', () => { +describe('StrictEffectsMode', () => { beforeEach(() => { jest.resetModules(); React = require('react'); @@ -27,7 +27,8 @@ describe('ReactDoubleInvokeEvents', () => { return gate( flags => flags.build === 'development' && - flags.enableDoubleInvokingEffects && + flags.enableStrictEffects && + flags.createRootStrictEffectsByDefault && flags.dfsEffectsRefactor, ); } diff --git a/packages/react-reconciler/src/__tests__/StrictEffectsModeDefaults-test.internal.js b/packages/react-reconciler/src/__tests__/StrictEffectsModeDefaults-test.internal.js new file mode 100644 index 0000000000000..469f43348beaf --- /dev/null +++ b/packages/react-reconciler/src/__tests__/StrictEffectsModeDefaults-test.internal.js @@ -0,0 +1,635 @@ +/** + * 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 + */ + +'use strict'; + +let React; +let ReactNoop; +let Scheduler; + +describe('StrictEffectsMode defaults', () => { + beforeEach(() => { + jest.resetModules(); + + React = require('react'); + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + + const ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.enableStrictEffects = __DEV__; + ReactFeatureFlags.createRootStrictEffectsByDefault = __DEV__; + }); + + it('should not double invoke effects in legacy mode', () => { + function App({text}) { + React.useEffect(() => { + Scheduler.unstable_yieldValue('useEffect mount'); + return () => Scheduler.unstable_yieldValue('useEffect unmount'); + }); + + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('useLayoutEffect mount'); + return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount'); + }); + + return text; + } + + ReactNoop.act(() => { + ReactNoop.renderLegacySyncRoot(); + }); + + expect(Scheduler).toHaveYielded([ + 'useLayoutEffect mount', + 'useEffect mount', + ]); + }); + + it('should not double invoke class lifecycles in legacy mode', () => { + class App extends React.PureComponent { + componentDidMount() { + Scheduler.unstable_yieldValue('componentDidMount'); + } + + componentDidUpdate() { + Scheduler.unstable_yieldValue('componentDidUpdate'); + } + + componentWillUnmount() { + Scheduler.unstable_yieldValue('componentWillUnmount'); + } + + render() { + return this.props.text; + } + } + + ReactNoop.act(() => { + ReactNoop.renderLegacySyncRoot(); + }); + + expect(Scheduler).toHaveYielded(['componentDidMount']); + }); + + if (__DEV__) { + it('should flush double-invoked effects within the same frame as layout effects if there are no passive effects', () => { + function ComponentWithEffects({label}) { + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue(`useLayoutEffect mount "${label}"`); + return () => + Scheduler.unstable_yieldValue(`useLayoutEffect unmount "${label}"`); + }); + + return label; + } + + ReactNoop.act(() => { + ReactNoop.render( + <> + + , + ); + + expect(Scheduler).toFlushUntilNextPaint([ + 'useLayoutEffect mount "one"', + 'useLayoutEffect unmount "one"', + 'useLayoutEffect mount "one"', + ]); + }); + + ReactNoop.act(() => { + ReactNoop.render( + <> + + + , + ); + + expect(Scheduler).toFlushUntilNextPaint([ + // Cleanup and re-run "one" (and "two") since there is no dependencies array. + 'useLayoutEffect unmount "one"', + 'useLayoutEffect mount "one"', + 'useLayoutEffect mount "two"', + + // Since "two" is new, it should be double-invoked. + 'useLayoutEffect unmount "two"', + 'useLayoutEffect mount "two"', + ]); + }); + }); + + // This test also verifies that double-invoked effects flush synchronously + // within the same frame as passive effects. + it('should double invoke effects only for newly mounted components', () => { + function ComponentWithEffects({label}) { + React.useEffect(() => { + Scheduler.unstable_yieldValue(`useEffect mount "${label}"`); + return () => + Scheduler.unstable_yieldValue(`useEffect unmount "${label}"`); + }); + + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue(`useLayoutEffect mount "${label}"`); + return () => + Scheduler.unstable_yieldValue(`useLayoutEffect unmount "${label}"`); + }); + + return label; + } + + ReactNoop.act(() => { + ReactNoop.render( + <> + + , + ); + + expect(Scheduler).toFlushAndYieldThrough([ + 'useLayoutEffect mount "one"', + ]); + expect(Scheduler).toFlushAndYield([ + 'useEffect mount "one"', + 'useLayoutEffect unmount "one"', + 'useEffect unmount "one"', + 'useLayoutEffect mount "one"', + 'useEffect mount "one"', + ]); + }); + + ReactNoop.act(() => { + ReactNoop.render( + <> + + + , + ); + + expect(Scheduler).toFlushAndYieldThrough([ + // Cleanup and re-run "one" (and "two") since there is no dependencies array. + 'useLayoutEffect unmount "one"', + 'useLayoutEffect mount "one"', + 'useLayoutEffect mount "two"', + ]); + expect(Scheduler).toFlushAndYield([ + 'useEffect unmount "one"', + 'useEffect mount "one"', + 'useEffect mount "two"', + + // Since "two" is new, it should be double-invoked. + 'useLayoutEffect unmount "two"', + 'useEffect unmount "two"', + 'useLayoutEffect mount "two"', + 'useEffect mount "two"', + ]); + }); + }); + + it('double invoking for effects for modern roots', () => { + function App({text}) { + React.useEffect(() => { + Scheduler.unstable_yieldValue('useEffect mount'); + return () => Scheduler.unstable_yieldValue('useEffect unmount'); + }); + + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('useLayoutEffect mount'); + return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount'); + }); + + return text; + } + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'useLayoutEffect mount', + 'useEffect mount', + 'useLayoutEffect unmount', + 'useEffect unmount', + 'useLayoutEffect mount', + 'useEffect mount', + ]); + + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'useLayoutEffect unmount', + 'useLayoutEffect mount', + 'useEffect unmount', + 'useEffect mount', + ]); + + ReactNoop.act(() => { + ReactNoop.render(null); + }); + + expect(Scheduler).toHaveYielded([ + 'useLayoutEffect unmount', + 'useEffect unmount', + ]); + }); + + it('multiple effects are double invoked in the right order (all mounted, all unmounted, all remounted)', () => { + function App({text}) { + React.useEffect(() => { + Scheduler.unstable_yieldValue('useEffect One mount'); + return () => Scheduler.unstable_yieldValue('useEffect One unmount'); + }); + + React.useEffect(() => { + Scheduler.unstable_yieldValue('useEffect Two mount'); + return () => Scheduler.unstable_yieldValue('useEffect Two unmount'); + }); + + return text; + } + + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'useEffect One mount', + 'useEffect Two mount', + 'useEffect One unmount', + 'useEffect Two unmount', + 'useEffect One mount', + 'useEffect Two mount', + ]); + + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'useEffect One unmount', + 'useEffect Two unmount', + 'useEffect One mount', + 'useEffect Two mount', + ]); + + ReactNoop.act(() => { + ReactNoop.render(null); + }); + + expect(Scheduler).toHaveYielded([ + 'useEffect One unmount', + 'useEffect Two unmount', + ]); + }); + + it('multiple layout effects are double invoked in the right order (all mounted, all unmounted, all remounted)', () => { + function App({text}) { + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('useLayoutEffect One mount'); + return () => + Scheduler.unstable_yieldValue('useLayoutEffect One unmount'); + }); + + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('useLayoutEffect Two mount'); + return () => + Scheduler.unstable_yieldValue('useLayoutEffect Two unmount'); + }); + + return text; + } + + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'useLayoutEffect One mount', + 'useLayoutEffect Two mount', + 'useLayoutEffect One unmount', + 'useLayoutEffect Two unmount', + 'useLayoutEffect One mount', + 'useLayoutEffect Two mount', + ]); + + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'useLayoutEffect One unmount', + 'useLayoutEffect Two unmount', + 'useLayoutEffect One mount', + 'useLayoutEffect Two mount', + ]); + + ReactNoop.act(() => { + ReactNoop.render(null); + }); + + expect(Scheduler).toHaveYielded([ + 'useLayoutEffect One unmount', + 'useLayoutEffect Two unmount', + ]); + }); + + it('useEffect and useLayoutEffect is called twice when there is no unmount', () => { + function App({text}) { + React.useEffect(() => { + Scheduler.unstable_yieldValue('useEffect mount'); + }); + + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('useLayoutEffect mount'); + }); + + return text; + } + + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'useLayoutEffect mount', + 'useEffect mount', + 'useLayoutEffect mount', + 'useEffect mount', + ]); + + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'useLayoutEffect mount', + 'useEffect mount', + ]); + + ReactNoop.act(() => { + ReactNoop.render(null); + }); + + expect(Scheduler).toHaveYielded([]); + }); + + it('passes the right context to class component lifecycles', () => { + class App extends React.PureComponent { + test() {} + + componentDidMount() { + this.test(); + Scheduler.unstable_yieldValue('componentDidMount'); + } + + componentDidUpdate() { + this.test(); + Scheduler.unstable_yieldValue('componentDidUpdate'); + } + + componentWillUnmount() { + this.test(); + Scheduler.unstable_yieldValue('componentWillUnmount'); + } + + render() { + return null; + } + } + + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'componentDidMount', + 'componentWillUnmount', + 'componentDidMount', + ]); + }); + + it('double invoking works for class components', () => { + class App extends React.PureComponent { + componentDidMount() { + Scheduler.unstable_yieldValue('componentDidMount'); + } + + componentDidUpdate() { + Scheduler.unstable_yieldValue('componentDidUpdate'); + } + + componentWillUnmount() { + Scheduler.unstable_yieldValue('componentWillUnmount'); + } + + render() { + return this.props.text; + } + } + + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'componentDidMount', + 'componentWillUnmount', + 'componentDidMount', + ]); + + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded(['componentDidUpdate']); + + ReactNoop.act(() => { + ReactNoop.render(null); + }); + + expect(Scheduler).toHaveYielded(['componentWillUnmount']); + }); + + it('double flushing passive effects only results in one double invoke', () => { + function App({text}) { + const [state, setState] = React.useState(0); + React.useEffect(() => { + if (state !== 1) { + setState(1); + } + Scheduler.unstable_yieldValue('useEffect mount'); + return () => Scheduler.unstable_yieldValue('useEffect unmount'); + }); + + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('useLayoutEffect mount'); + return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount'); + }); + + Scheduler.unstable_yieldValue(text); + return text; + } + + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'mount', + 'useLayoutEffect mount', + 'useEffect mount', + 'useLayoutEffect unmount', + 'useEffect unmount', + 'useLayoutEffect mount', + 'useEffect mount', + 'mount', + 'useLayoutEffect unmount', + 'useLayoutEffect mount', + 'useEffect unmount', + 'useEffect mount', + ]); + }); + + it('newly mounted components after initial mount get double invoked', () => { + let _setShowChild; + function Child() { + React.useEffect(() => { + Scheduler.unstable_yieldValue('Child useEffect mount'); + return () => Scheduler.unstable_yieldValue('Child useEffect unmount'); + }); + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Child useLayoutEffect mount'); + return () => + Scheduler.unstable_yieldValue('Child useLayoutEffect unmount'); + }); + + return null; + } + + function App() { + const [showChild, setShowChild] = React.useState(false); + _setShowChild = setShowChild; + React.useEffect(() => { + Scheduler.unstable_yieldValue('App useEffect mount'); + return () => Scheduler.unstable_yieldValue('App useEffect unmount'); + }); + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('App useLayoutEffect mount'); + return () => + Scheduler.unstable_yieldValue('App useLayoutEffect unmount'); + }); + + return showChild && ; + } + + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'App useLayoutEffect mount', + 'App useEffect mount', + 'App useLayoutEffect unmount', + 'App useEffect unmount', + 'App useLayoutEffect mount', + 'App useEffect mount', + ]); + + ReactNoop.act(() => { + _setShowChild(true); + }); + + expect(Scheduler).toHaveYielded([ + 'App useLayoutEffect unmount', + 'Child useLayoutEffect mount', + 'App useLayoutEffect mount', + 'App useEffect unmount', + 'Child useEffect mount', + 'App useEffect mount', + 'Child useLayoutEffect unmount', + 'Child useEffect unmount', + 'Child useLayoutEffect mount', + 'Child useEffect mount', + ]); + }); + + it('classes and functions are double invoked together correctly', () => { + class ClassChild extends React.PureComponent { + componentDidMount() { + Scheduler.unstable_yieldValue('componentDidMount'); + } + + componentWillUnmount() { + Scheduler.unstable_yieldValue('componentWillUnmount'); + } + + render() { + return this.props.text; + } + } + + function FunctionChild({text}) { + React.useEffect(() => { + Scheduler.unstable_yieldValue('useEffect mount'); + return () => Scheduler.unstable_yieldValue('useEffect unmount'); + }); + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('useLayoutEffect mount'); + return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount'); + }); + return text; + } + + function App({text}) { + return ( + <> + + + + ); + } + + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'componentDidMount', + 'useLayoutEffect mount', + 'useEffect mount', + 'componentWillUnmount', + 'useLayoutEffect unmount', + 'useEffect unmount', + 'componentDidMount', + 'useLayoutEffect mount', + 'useEffect mount', + ]); + + ReactNoop.act(() => { + ReactNoop.render(); + }); + + expect(Scheduler).toHaveYielded([ + 'useLayoutEffect unmount', + 'useLayoutEffect mount', + 'useEffect unmount', + 'useEffect mount', + ]); + + ReactNoop.act(() => { + ReactNoop.render(null); + }); + + expect(Scheduler).toHaveYielded([ + 'componentWillUnmount', + 'useLayoutEffect unmount', + 'useEffect unmount', + ]); + }); + } +}); diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index c4eea12e8a550..ff80440379f14 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -451,6 +451,7 @@ function create(element: React$Element, options: TestRendererOptions) { isConcurrent ? ConcurrentRoot : LegacyRoot, false, null, + null, ); invariant(root != null, 'something went wrong'); updateContainer(element, root, null, null); diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index a2e27073a3f79..d6e44ab820960 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -4875,7 +4875,8 @@ describe('Profiler', () => { if (__DEV__) { it('double invoking does not disconnect wrapped async work', () => { - ReactFeatureFlags.enableDoubleInvokingEffects = true; + ReactFeatureFlags.enableStrictEffects = true; + ReactFeatureFlags.createRootStrictEffectsByDefault = true; const callback = jest.fn(() => { const wrappedInteractions = SchedulerTracing.unstable_getCurrent(); diff --git a/packages/react/src/__tests__/ReactStrictMode-test.internal.js b/packages/react/src/__tests__/ReactStrictMode-test.internal.js new file mode 100644 index 0000000000000..cb567340d3983 --- /dev/null +++ b/packages/react/src/__tests__/ReactStrictMode-test.internal.js @@ -0,0 +1,264 @@ +/** + * 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 + */ + +'use strict'; + +describe('ReactStrictMode', () => { + let React; + let ReactDOM; + let act; + + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactDOM = require('react-dom'); + + const TestUtils = require('react-dom/test-utils'); + act = TestUtils.unstable_concurrentAct; + + const ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.enableStrictEffects = __DEV__; + }); + + describe('levels', () => { + let log; + + beforeEach(() => { + log = []; + }); + + function Component({label}) { + React.useEffect(() => { + log.push(`${label}: useEffect mount`); + return () => log.push(`${label}: useEffect unmount`); + }); + + React.useLayoutEffect(() => { + log.push(`${label}: useLayoutEffect mount`); + return () => log.push(`${label}: useLayoutEffect unmount`); + }); + + log.push(`${label}: render`); + + return null; + } + + // @gate experimental + it('should support overriding default via createRoot option', () => { + act(() => { + const container = document.createElement('div'); + const root = ReactDOM.createRoot(container, { + unstable_strictModeLevel: 0, + }); + root.render(); + }); + + expect(log).toEqual([ + 'A: render', + 'A: useLayoutEffect mount', + 'A: useEffect mount', + ]); + }); + + // @gate experimental + it('should support overriding default via createBlockingRoot option', () => { + act(() => { + const container = document.createElement('div'); + const root = ReactDOM.createBlockingRoot(container, { + unstable_strictModeLevel: 0, + }); + root.render(); + }); + + expect(log).toEqual([ + 'A: render', + 'A: useLayoutEffect mount', + 'A: useEffect mount', + ]); + }); + + // @gate experimental + it('should disable strict mode if level 0 is specified', () => { + act(() => { + const container = document.createElement('div'); + const root = ReactDOM.createRoot(container, { + unstable_strictModeLevel: 0, + }); + root.render( + + + , + ); + }); + + expect(log).toEqual([ + 'A: render', + 'A: useLayoutEffect mount', + 'A: useEffect mount', + ]); + }); + + if (__DEV__) { + // @gate experimental + it('should default to level 1 (legacy mode)', () => { + act(() => { + const container = document.createElement('div'); + const root = ReactDOM.createRoot(container); + root.render( + + + , + ); + }); + + expect(log).toEqual([ + 'A: render', + 'A: render', + 'A: useLayoutEffect mount', + 'A: useEffect mount', + ]); + }); + + // @gate experimental + it('should support level 1 (legacy mode)', () => { + act(() => { + const container = document.createElement('div'); + const root = ReactDOM.createRoot(container); + root.render( + + + , + ); + }); + + expect(log).toEqual([ + 'A: render', + 'A: render', + 'A: useLayoutEffect mount', + 'A: useEffect mount', + ]); + }); + + // @gate experimental + it('should support level 2 (legacy + strict effects mode)', () => { + act(() => { + const container = document.createElement('div'); + const root = ReactDOM.createRoot(container); + root.render( + + + , + ); + }); + + expect(log).toEqual([ + 'A: render', + 'A: render', + 'A: useLayoutEffect mount', + 'A: useEffect mount', + 'A: useLayoutEffect unmount', + 'A: useEffect unmount', + 'A: useLayoutEffect mount', + 'A: useEffect mount', + ]); + }); + + // @gate experimental + it('should allow level to be increased with nesting', () => { + act(() => { + const container = document.createElement('div'); + const root = ReactDOM.createRoot(container, { + unstable_strictModeLevel: 0, + }); + root.render( + <> + + + + + + + , + + , + , + ); + }); + + expect(log).toEqual([ + 'A: render', + 'B: render', + 'B: render', + 'C: render', + 'C: render', + 'A: useLayoutEffect mount', + 'B: useLayoutEffect mount', + 'C: useLayoutEffect mount', + 'A: useEffect mount', + 'B: useEffect mount', + 'C: useEffect mount', + 'C: useLayoutEffect unmount', + 'C: useEffect unmount', + 'C: useLayoutEffect mount', + 'C: useEffect mount', + ]); + }); + + // @gate experimental + it('should not allow level to be decreased with nesting', () => { + act(() => { + const container = document.createElement('div'); + const root = ReactDOM.createRoot(container, { + unstable_strictModeLevel: 2, + }); + root.render( + <> + + + + + + + , + + , + , + ); + }); + + expect(log).toEqual([ + 'A: render', + 'A: render', + 'B: render', + 'B: render', + 'C: render', + 'C: render', + 'A: useLayoutEffect mount', + 'B: useLayoutEffect mount', + 'C: useLayoutEffect mount', + 'A: useEffect mount', + 'B: useEffect mount', + 'C: useEffect mount', + 'A: useLayoutEffect unmount', + 'B: useLayoutEffect unmount', + 'C: useLayoutEffect unmount', + 'A: useEffect unmount', + 'B: useEffect unmount', + 'C: useEffect unmount', + 'A: useLayoutEffect mount', + 'B: useLayoutEffect mount', + 'C: useLayoutEffect mount', + 'A: useEffect mount', + 'B: useEffect mount', + 'C: useEffect mount', + ]); + }); + } + }); +}); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 778881bc5837d..953eb7cd56615 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -20,9 +20,17 @@ export const enableDebugTracing = false; export const enableSchedulingProfiler = __PROFILE__ && __EXPERIMENTAL__; // Helps identify side effects in render-phase lifecycle hooks and setState -// reducers by double invoking them in Strict Mode. +// reducers by double invoking them in StrictLegacyMode. export const debugRenderPhaseSideEffectsForStrictMode = __DEV__; +// Helps identify code that is not safe for planned Offscreen API and Suspense semantics; +// this feature flag only impacts StrictEffectsMode. +export const enableStrictEffects = false; + +// If TRUE, trees rendered with createRoot (and createBlockingRoot) APIs will be StrictEffectsMode. +// If FALSE, these trees will be StrictLegacyMode. +export const createRootStrictEffectsByDefault = false; + // To preserve the "Pause on caught exceptions" behavior of the debugger, we // replay the begin phase of a failed component inside invokeGuardedCallback. export const replayFailedUnitOfWorkWithInvokeGuardedCallback = __DEV__; @@ -139,8 +147,6 @@ export const decoupleUpdatePriorityFromScheduler = false; export const enableDiscreteEventFlushingChange = false; -export const enableDoubleInvokingEffects = false; - export const enableUseRefAccessWarning = false; export const enableRecursiveCommitTraversal = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 6544be4cbe040..2f4406b2ceafa 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -52,7 +52,8 @@ export const deferRenderPhaseUpdateToNextBatch = true; export const decoupleUpdatePriorityFromScheduler = false; export const enableDiscreteEventFlushingChange = false; -export const enableDoubleInvokingEffects = false; +export const enableStrictEffects = false; +export const createRootStrictEffectsByDefault = false; export const enableUseRefAccessWarning = false; export const enableRecursiveCommitTraversal = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index a6482da52d524..35e1067646d94 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -51,7 +51,8 @@ export const deferRenderPhaseUpdateToNextBatch = true; export const decoupleUpdatePriorityFromScheduler = false; export const enableDiscreteEventFlushingChange = false; -export const enableDoubleInvokingEffects = false; +export const enableStrictEffects = false; +export const createRootStrictEffectsByDefault = false; export const enableUseRefAccessWarning = false; export const enableRecursiveCommitTraversal = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 343bb609b4d25..67ecdb72983c7 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -51,7 +51,8 @@ export const deferRenderPhaseUpdateToNextBatch = true; export const decoupleUpdatePriorityFromScheduler = false; export const enableDiscreteEventFlushingChange = false; -export const enableDoubleInvokingEffects = false; +export const enableStrictEffects = false; +export const createRootStrictEffectsByDefault = false; export const enableUseRefAccessWarning = false; export const enableRecursiveCommitTraversal = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index e9ad4240a6fc3..9ed0be59689e7 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -51,7 +51,8 @@ export const deferRenderPhaseUpdateToNextBatch = true; export const decoupleUpdatePriorityFromScheduler = false; export const enableDiscreteEventFlushingChange = false; -export const enableDoubleInvokingEffects = false; +export const enableStrictEffects = false; +export const createRootStrictEffectsByDefault = false; export const enableUseRefAccessWarning = false; export const enableRecursiveCommitTraversal = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 2afc959952aca..2e7513a8ddf15 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -51,7 +51,8 @@ export const deferRenderPhaseUpdateToNextBatch = true; export const decoupleUpdatePriorityFromScheduler = false; export const enableDiscreteEventFlushingChange = false; -export const enableDoubleInvokingEffects = true; +export const enableStrictEffects = true; +export const createRootStrictEffectsByDefault = false; export const enableUseRefAccessWarning = false; export const enableRecursiveCommitTraversal = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index d79a63c577892..5a362e8a31419 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -51,7 +51,8 @@ export const deferRenderPhaseUpdateToNextBatch = true; export const decoupleUpdatePriorityFromScheduler = false; export const enableDiscreteEventFlushingChange = false; -export const enableDoubleInvokingEffects = false; +export const enableStrictEffects = false; +export const createRootStrictEffectsByDefault = false; export const enableUseRefAccessWarning = false; export const enableRecursiveCommitTraversal = false; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 732360618f6bf..54942f0c7cd2d 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -51,7 +51,8 @@ export const deferRenderPhaseUpdateToNextBatch = true; export const decoupleUpdatePriorityFromScheduler = false; export const enableDiscreteEventFlushingChange = true; -export const enableDoubleInvokingEffects = false; +export const enableStrictEffects = false; +export const createRootStrictEffectsByDefault = false; export const enableUseRefAccessWarning = false; export const enableRecursiveCommitTraversal = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index daf49b2ab2255..1d9f89699f317 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -50,7 +50,8 @@ export const enableTrustedTypesIntegration = false; export const disableSchedulerTimeoutBasedOnReactExpirationTime = false; export const disableNativeComponentFrames = false; -export const enableDoubleInvokingEffects = false; +export const createRootStrictEffectsByDefault = false; +export const enableStrictEffects = false; export const enableUseRefAccessWarning = __VARIANT__; export const enableProfilerNestedUpdateScheduledHook = __VARIANT__; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 2d983a3e8f81d..5e5cd2105b383 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -27,7 +27,8 @@ export const { decoupleUpdatePriorityFromScheduler, enableDebugTracing, skipUnmountedBoundaries, - enableDoubleInvokingEffects, + enableStrictEffects, + createRootStrictEffectsByDefault, enableUseRefAccessWarning, disableNativeComponentFrames, disableSchedulerTimeoutInWorkLoop,