diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js index 69f1ab744498c..d1a4837fdba01 100644 --- a/packages/react-reconciler/src/ReactFiber.new.js +++ b/packages/react-reconciler/src/ReactFiber.new.js @@ -19,8 +19,9 @@ import type {OffscreenProps} from './ReactFiberOffscreenComponent'; import invariant from 'shared/invariant'; import { + createRootStrictEffectsByDefault, enableCache, - enableDoubleInvokingEffects, + enableStrictEffects, enableProfilerTimer, enableScopeAPI, } from 'shared/ReactFeatureFlags'; @@ -423,14 +424,14 @@ export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) { export function createHostRootFiber(tag: RootTag): Fiber { let mode; if (tag === ConcurrentRoot) { - if (enableDoubleInvokingEffects) { + if (enableStrictEffects && createRootStrictEffectsByDefault) { mode = ConcurrentMode | BlockingMode | StrictLegacyMode | StrictEffectsMode; } else { mode = ConcurrentMode | BlockingMode | StrictLegacyMode; } } else if (tag === BlockingRoot) { - if (enableDoubleInvokingEffects) { + if (enableStrictEffects && createRootStrictEffectsByDefault) { mode = BlockingMode | StrictLegacyMode | StrictEffectsMode; } else { mode = BlockingMode | StrictLegacyMode; diff --git a/packages/react-reconciler/src/ReactFiber.old.js b/packages/react-reconciler/src/ReactFiber.old.js index 3f083c83c158a..6f79f0331d348 100644 --- a/packages/react-reconciler/src/ReactFiber.old.js +++ b/packages/react-reconciler/src/ReactFiber.old.js @@ -19,8 +19,9 @@ import type {OffscreenProps} from './ReactFiberOffscreenComponent'; import invariant from 'shared/invariant'; import { + createRootStrictEffectsByDefault, enableCache, - enableDoubleInvokingEffects, + enableStrictEffects, enableProfilerTimer, enableScopeAPI, } from 'shared/ReactFeatureFlags'; @@ -423,14 +424,14 @@ export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) { export function createHostRootFiber(tag: RootTag): Fiber { let mode; if (tag === ConcurrentRoot) { - if (enableDoubleInvokingEffects) { + if (enableStrictEffects && createRootStrictEffectsByDefault) { mode = ConcurrentMode | BlockingMode | StrictLegacyMode | StrictEffectsMode; } else { mode = ConcurrentMode | BlockingMode | StrictLegacyMode; } } else if (tag === BlockingRoot) { - if (enableDoubleInvokingEffects) { + if (enableStrictEffects && createRootStrictEffectsByDefault) { mode = BlockingMode | StrictLegacyMode | StrictEffectsMode; } else { mode = BlockingMode | StrictLegacyMode; diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.new.js b/packages/react-reconciler/src/ReactFiberClassComponent.new.js index 6a4c156feb667..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'; @@ -908,7 +908,7 @@ function mountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. @@ -987,7 +987,7 @@ function resumeMountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. @@ -1039,7 +1039,7 @@ function resumeMountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. @@ -1054,7 +1054,7 @@ function resumeMountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.old.js b/packages/react-reconciler/src/ReactFiberClassComponent.old.js index 1c81ce7c6e549..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'; @@ -908,7 +908,7 @@ function mountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. @@ -987,7 +987,7 @@ function resumeMountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. @@ -1039,7 +1039,7 @@ function resumeMountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. @@ -1054,7 +1054,7 @@ function resumeMountClassInstance( if (typeof instance.componentDidMount === 'function') { if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (workInProgress.mode & StrictEffectsMode) !== NoMode ) { // Never double-invoke effects for legacy roots. diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index dbf2ee7805f50..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,7 +2475,7 @@ function ensureCorrectReturnPointer(fiber, expectedReturnFiber) { } function invokeLayoutEffectMountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { + 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) { @@ -2509,7 +2509,7 @@ function invokeLayoutEffectMountInDEV(fiber: Fiber): void { } function invokePassiveEffectMountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { + 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) { @@ -2534,7 +2534,7 @@ function invokePassiveEffectMountInDEV(fiber: Fiber): void { } function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { + 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) { @@ -2578,7 +2578,7 @@ function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void { } function invokePassiveEffectUnmountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { + 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) { diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index cbe7439af1de5..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,7 +2475,7 @@ function ensureCorrectReturnPointer(fiber, expectedReturnFiber) { } function invokeLayoutEffectMountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { + 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) { @@ -2509,7 +2509,7 @@ function invokeLayoutEffectMountInDEV(fiber: Fiber): void { } function invokePassiveEffectMountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { + 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) { @@ -2534,7 +2534,7 @@ function invokePassiveEffectMountInDEV(fiber: Fiber): void { } function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { + 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) { @@ -2578,7 +2578,7 @@ function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void { } function invokePassiveEffectUnmountInDEV(fiber: Fiber): void { - if (__DEV__ && enableDoubleInvokingEffects) { + 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) { diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index be92331fa2f46..9d1fa5f82ed8e 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -29,7 +29,7 @@ import { enableCache, decoupleUpdatePriorityFromScheduler, enableUseRefAccessWarning, - enableDoubleInvokingEffects, + enableStrictEffects, } from 'shared/ReactFeatureFlags'; import { @@ -509,7 +509,7 @@ export function bailoutHooks( // complete phase (bubbleProperties). if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (workInProgress.mode & StrictEffectsMode) !== NoMode ) { workInProgress.flags &= ~( @@ -1423,7 +1423,7 @@ function mountEffect( } if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode ) { return mountEffectImpl( @@ -1461,7 +1461,7 @@ function mountLayoutEffect( ): void { if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode ) { return mountEffectImpl( @@ -1533,7 +1533,7 @@ function mountImperativeHandle( if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode ) { return mountEffectImpl( @@ -1832,7 +1832,7 @@ function mountOpaqueIdentifier(): OpaqueIDType | void { if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) { if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (currentlyRenderingFiber.mode & StrictEffectsMode) === NoMode ) { currentlyRenderingFiber.flags |= MountPassiveDevEffect | PassiveEffect; diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index b8b7ca2f56a57..61f1a17452e4b 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -29,7 +29,7 @@ import { enableCache, decoupleUpdatePriorityFromScheduler, enableUseRefAccessWarning, - enableDoubleInvokingEffects, + enableStrictEffects, } from 'shared/ReactFeatureFlags'; import { @@ -509,7 +509,7 @@ export function bailoutHooks( // complete phase (bubbleProperties). if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (workInProgress.mode & StrictEffectsMode) !== NoMode ) { workInProgress.flags &= ~( @@ -1423,7 +1423,7 @@ function mountEffect( } if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode ) { return mountEffectImpl( @@ -1461,7 +1461,7 @@ function mountLayoutEffect( ): void { if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode ) { return mountEffectImpl( @@ -1533,7 +1533,7 @@ function mountImperativeHandle( if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode ) { return mountEffectImpl( @@ -1832,7 +1832,7 @@ function mountOpaqueIdentifier(): OpaqueIDType | void { if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) { if ( __DEV__ && - enableDoubleInvokingEffects && + enableStrictEffects && (currentlyRenderingFiber.mode & StrictEffectsMode) === NoMode ) { currentlyRenderingFiber.flags |= MountPassiveDevEffect | PassiveEffect; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index eb6c139486b38..ebc9fb8f17605 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'; @@ -2071,7 +2071,7 @@ function commitRootImpl(root, renderPriorityLevel) { legacyErrorBoundariesThatAlreadyFailed = null; } - if (__DEV__ && enableDoubleInvokingEffects) { + if (__DEV__ && enableStrictEffects) { if (!rootDidHavePassiveEffects) { commitDoubleInvokeEffectsInDEV(root.current, false); } @@ -2258,7 +2258,7 @@ function flushPassiveEffectsImpl() { markPassiveEffectsStopped(); } - if (__DEV__ && enableDoubleInvokingEffects) { + if (__DEV__ && enableStrictEffects) { commitDoubleInvokeEffectsInDEV(root.current, true); } @@ -2561,7 +2561,7 @@ function commitDoubleInvokeEffectsInDEV( fiber: Fiber, hasPassiveEffects: boolean, ) { - if (__DEV__ && enableDoubleInvokingEffects) { + if (__DEV__ && enableStrictEffects) { // Never double-invoke effects outside of StrictEffectsMode. if ((fiber.mode & StrictEffectsMode) === NoMode) { return; @@ -2590,7 +2590,7 @@ function invokeEffectsInDev( fiberFlags: Flags, invokeEffectFn: (fiber: Fiber) => void, ): void { - if (__DEV__ && enableDoubleInvokingEffects) { + if (__DEV__ && enableStrictEffects) { // We don't need to re-check StrictEffectsMode here. // This function is only called if that check has already passed. diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 049d9387ba4ba..25b77701d8699 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'; @@ -2071,7 +2071,7 @@ function commitRootImpl(root, renderPriorityLevel) { legacyErrorBoundariesThatAlreadyFailed = null; } - if (__DEV__ && enableDoubleInvokingEffects) { + if (__DEV__ && enableStrictEffects) { if (!rootDidHavePassiveEffects) { commitDoubleInvokeEffectsInDEV(root.current, false); } @@ -2258,7 +2258,7 @@ function flushPassiveEffectsImpl() { markPassiveEffectsStopped(); } - if (__DEV__ && enableDoubleInvokingEffects) { + if (__DEV__ && enableStrictEffects) { commitDoubleInvokeEffectsInDEV(root.current, true); } @@ -2561,7 +2561,7 @@ function commitDoubleInvokeEffectsInDEV( fiber: Fiber, hasPassiveEffects: boolean, ) { - if (__DEV__ && enableDoubleInvokingEffects) { + if (__DEV__ && enableStrictEffects) { // Never double-invoke effects outside of StrictEffectsMode. if ((fiber.mode & StrictEffectsMode) === NoMode) { return; @@ -2590,7 +2590,7 @@ function invokeEffectsInDev( fiberFlags: Flags, invokeEffectFn: (fiber: Fiber) => void, ): void { - if (__DEV__ && enableDoubleInvokingEffects) { + if (__DEV__ && enableStrictEffects) { // We don't need to re-check StrictEffectsMode here. // This function is only called if that check has already passed. 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/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/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index c12fb8b8d0d3c..bd8fbc8de68af 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -25,7 +25,11 @@ 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 enableDoubleInvokingEffects = false; +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. diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index f97f4f428c181..fe0fbe6bc91d3 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 9d160790b3fec..a0be8bf464c7d 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 52990e5ec7b71..9751992ced291 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 3c739b7bccb39..cdc0361693090 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 fb2211763c03d..6855d614736f2 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 34e6d5315742f..83514ba2ed3c6 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 4f0c56733eafd..38713e81dc4ed 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 b6e1d4260cc4a..044604cee16d2 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 924fd6ea9e5bd..77de1bc8aa5c0 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,