diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js index 6cdb179093b12..2f534013aa2e6 100644 --- a/packages/react-native-renderer/src/ReactFabric.js +++ b/packages/react-native-renderer/src/ReactFabric.js @@ -48,6 +48,7 @@ import {getPublicInstanceFromInternalInstanceHandle} from './ReactFiberConfigFab // Module provided by RN: import {ReactFiberErrorDialog} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; +import {disableLegacyMode} from 'shared/ReactFeatureFlags'; if (typeof ReactFiberErrorDialog.showErrorDialog !== 'function') { throw new Error( @@ -106,6 +107,10 @@ function render( callback: ?() => void, concurrentRoot: ?boolean, ): ?ElementRef { + if (disableLegacyMode && !concurrentRoot) { + throw new Error('render: Unsupported Legacy Mode API.'); + } + let root = roots.get(containerTag); if (!root) { diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index 5409eb3df4f98..7c5410cfe4473 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -50,6 +50,8 @@ import { isChildPublicInstance, } from './ReactNativePublicCompat'; +import {disableLegacyMode} from 'shared/ReactFeatureFlags'; + // Module provided by RN: import {ReactFiberErrorDialog} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; @@ -109,6 +111,10 @@ function render( containerTag: number, callback: ?() => void, ): ?ElementRef { + if (disableLegacyMode) { + throw new Error('render: Unsupported Legacy Mode API.'); + } + let root = roots.get(containerTag); if (!root) { diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeError-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeError-test.internal.js index b056f7a80229a..a7ac05b214492 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeError-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeError-test.internal.js @@ -50,6 +50,7 @@ describe('ReactNativeError', () => { ); }); + // @gate !disableLegacyMode it('should be able to extract a component stack from a native view', () => { const View = createReactNativeComponentClass('View', () => ({ validAttributes: {foo: true}, diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js index f210de5f8c456..fac0b8b5c3800 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js @@ -79,6 +79,7 @@ beforeEach(() => { .ReactNativeViewConfigRegistry.register; }); +// @gate !disableLegacyMode it('fails to register the same event name with different types', async () => { const InvalidEvents = createReactNativeComponentClass('InvalidEvents', () => { if (!__DEV__) { @@ -122,6 +123,7 @@ it('fails to register the same event name with different types', async () => { ).rejects.toThrow('Event cannot be both direct and bubbling: topChange'); }); +// @gate !disableLegacyMode it('fails if unknown/unsupported event types are dispatched', () => { expect(RCTEventEmitter.register).toHaveBeenCalledTimes(1); const EventEmitter = RCTEventEmitter.register.mock.calls[0][0]; @@ -129,7 +131,10 @@ it('fails if unknown/unsupported event types are dispatched', () => { ReactNative.render( {}} />, 1); - expect(UIManager.__dumpHierarchyForJestTestsOnly()).toMatchSnapshot(); + expect(UIManager.__dumpHierarchyForJestTestsOnly()).toMatchInlineSnapshot(` + " {} + View null" + `); expect(UIManager.createView).toHaveBeenCalledTimes(1); const target = UIManager.createView.mock.calls[0][0]; @@ -143,6 +148,7 @@ it('fails if unknown/unsupported event types are dispatched', () => { }).toThrow('Unsupported top level event type "unspecifiedEvent" dispatched'); }); +// @gate !disableLegacyMode it('handles events', () => { expect(RCTEventEmitter.register).toHaveBeenCalledTimes(1); const EventEmitter = RCTEventEmitter.register.mock.calls[0][0]; @@ -167,7 +173,11 @@ it('handles events', () => { 1, ); - expect(UIManager.__dumpHierarchyForJestTestsOnly()).toMatchSnapshot(); + expect(UIManager.__dumpHierarchyForJestTestsOnly()).toMatchInlineSnapshot(` + " {} + View {"foo":"outer"} + View {"foo":"inner"}" + `); expect(UIManager.createView).toHaveBeenCalledTimes(2); // Don't depend on the order of createView() calls. @@ -200,6 +210,7 @@ it('handles events', () => { }); // @gate !disableLegacyContext || !__DEV__ +// @gate !disableLegacyMode it('handles events on text nodes', () => { expect(RCTEventEmitter.register).toHaveBeenCalledTimes(1); const EventEmitter = RCTEventEmitter.register.mock.calls[0][0]; @@ -283,6 +294,7 @@ it('handles events on text nodes', () => { ]); }); +// @gate !disableLegacyMode it('handles when a responder is unmounted while a touch sequence is in progress', () => { const EventEmitter = RCTEventEmitter.register.mock.calls[0][0]; const View = fakeRequireNativeComponent('View', {id: true}); @@ -372,6 +384,7 @@ it('handles when a responder is unmounted while a touch sequence is in progress' expect(log).toEqual(['two responder start']); }); +// @gate !disableLegacyMode it('handles events without target', () => { const EventEmitter = RCTEventEmitter.register.mock.calls[0][0]; @@ -462,6 +475,7 @@ it('handles events without target', () => { ]); }); +// @gate !disableLegacyMode it('dispatches event with target as instance', () => { const EventEmitter = RCTEventEmitter.register.mock.calls[0][0]; diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js index 06604f2d062f9..c2c06a0bf2b63 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js @@ -45,6 +45,7 @@ describe('ReactNative', () => { require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface').TextInputState; }); + // @gate !disableLegacyMode it('should be able to create and render a native component', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -58,6 +59,7 @@ describe('ReactNative', () => { expect(UIManager.updateView).not.toBeCalled(); }); + // @gate !disableLegacyMode it('should be able to create and update a native component', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -79,6 +81,7 @@ describe('ReactNative', () => { expect(UIManager.updateView).toBeCalledWith(3, 'RCTView', {foo: 'bar'}); }); + // @gate !disableLegacyMode it('should not call UIManager.updateView after render for properties that have not changed', () => { const Text = createReactNativeComponentClass('RCTText', () => ({ validAttributes: {foo: true}, @@ -105,6 +108,7 @@ describe('ReactNative', () => { expect(UIManager.updateView).toHaveBeenCalledTimes(4); }); + // @gate !disableLegacyMode it('should call dispatchCommand for native refs', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -133,6 +137,7 @@ describe('ReactNative', () => { ); }); + // @gate !disableLegacyMode it('should warn and no-op if calling dispatchCommand on non native refs', () => { class BasicClass extends React.Component { render() { @@ -162,6 +167,7 @@ describe('ReactNative', () => { expect(UIManager.dispatchViewManagerCommand).not.toBeCalled(); }); + // @gate !disableLegacyMode it('should call sendAccessibilityEvent for native refs', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -192,6 +198,7 @@ describe('ReactNative', () => { ).toHaveBeenCalledWith(expect.any(Number), 'focus'); }); + // @gate !disableLegacyMode it('should warn and no-op if calling sendAccessibilityEvent on non native refs', () => { class BasicClass extends React.Component { render() { @@ -221,6 +228,7 @@ describe('ReactNative', () => { expect(UIManager.sendAccessibilityEvent).not.toBeCalled(); }); + // @gate !disableLegacyMode it('should not call UIManager.updateView from ref.setNativeProps for properties that have not changed', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -254,6 +262,7 @@ describe('ReactNative', () => { ); }); + // @gate !disableLegacyMode it('should call UIManager.measure on ref.measure', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -280,6 +289,7 @@ describe('ReactNative', () => { expect(successCallback).toHaveBeenCalledWith(10, 10, 100, 100, 0, 0); }); + // @gate !disableLegacyMode it('should call UIManager.measureInWindow on ref.measureInWindow', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -306,6 +316,7 @@ describe('ReactNative', () => { expect(successCallback).toHaveBeenCalledWith(10, 10, 100, 100); }); + // @gate !disableLegacyMode it('should support reactTag in ref.measureLayout', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -346,6 +357,7 @@ describe('ReactNative', () => { expect(successCallback).toHaveBeenCalledWith(1, 1, 100, 100); }); + // @gate !disableLegacyMode it('should support ref in ref.measureLayout of host components', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -382,6 +394,7 @@ describe('ReactNative', () => { expect(successCallback).toHaveBeenCalledWith(1, 1, 100, 100); }); + // @gate !disableLegacyMode it('returns the correct instance and calls it in the callback', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -403,6 +416,7 @@ describe('ReactNative', () => { expect(a).toBe(c); }); + // @gate !disableLegacyMode it('renders and reorders children', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {title: true}, @@ -427,12 +441,59 @@ describe('ReactNative', () => { const after = 'mxhpgwfralkeoivcstzy'; ReactNative.render(, 11); - expect(UIManager.__dumpHierarchyForJestTestsOnly()).toMatchSnapshot(); + expect(UIManager.__dumpHierarchyForJestTestsOnly()).toMatchInlineSnapshot(` + " {} + RCTView null + RCTView {"title":"a"} + RCTView {"title":"b"} + RCTView {"title":"c"} + RCTView {"title":"d"} + RCTView {"title":"e"} + RCTView {"title":"f"} + RCTView {"title":"g"} + RCTView {"title":"h"} + RCTView {"title":"i"} + RCTView {"title":"j"} + RCTView {"title":"k"} + RCTView {"title":"l"} + RCTView {"title":"m"} + RCTView {"title":"n"} + RCTView {"title":"o"} + RCTView {"title":"p"} + RCTView {"title":"q"} + RCTView {"title":"r"} + RCTView {"title":"s"} + RCTView {"title":"t"}" + `); ReactNative.render(, 11); - expect(UIManager.__dumpHierarchyForJestTestsOnly()).toMatchSnapshot(); + expect(UIManager.__dumpHierarchyForJestTestsOnly()).toMatchInlineSnapshot(` + " {} + RCTView null + RCTView {"title":"m"} + RCTView {"title":"x"} + RCTView {"title":"h"} + RCTView {"title":"p"} + RCTView {"title":"g"} + RCTView {"title":"w"} + RCTView {"title":"f"} + RCTView {"title":"r"} + RCTView {"title":"a"} + RCTView {"title":"l"} + RCTView {"title":"k"} + RCTView {"title":"e"} + RCTView {"title":"o"} + RCTView {"title":"i"} + RCTView {"title":"v"} + RCTView {"title":"c"} + RCTView {"title":"s"} + RCTView {"title":"t"} + RCTView {"title":"z"} + RCTView {"title":"y"}" + `); }); + // @gate !disableLegacyMode it('calls setState with no arguments', () => { let mockArgs; class Component extends React.Component { @@ -448,6 +509,7 @@ describe('ReactNative', () => { expect(mockArgs.length).toEqual(0); }); + // @gate !disableLegacyMode it('should not throw when is used inside of a ancestor', () => { const Image = createReactNativeComponentClass('RCTImage', () => ({ validAttributes: {}, @@ -478,6 +540,7 @@ describe('ReactNative', () => { ); }); + // @gate !disableLegacyMode it('should throw for text not inside of a ancestor', async () => { const ScrollView = createReactNativeComponentClass('RCTScrollView', () => ({ validAttributes: {}, @@ -512,6 +575,7 @@ describe('ReactNative', () => { ); }); + // @gate !disableLegacyMode it('should not throw for text inside of an indirect ancestor', () => { const Text = createReactNativeComponentClass('RCTText', () => ({ validAttributes: {}, @@ -528,6 +592,7 @@ describe('ReactNative', () => { ); }); + // @gate !disableLegacyMode it('findHostInstance_DEPRECATED should warn if used to find a host component inside StrictMode', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -564,6 +629,7 @@ describe('ReactNative', () => { expect(match).toBe(child); }); + // @gate !disableLegacyMode it('findHostInstance_DEPRECATED should warn if passed a component that is inside StrictMode', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -601,6 +667,7 @@ describe('ReactNative', () => { expect(match).toBe(child); }); + // @gate !disableLegacyMode it('findNodeHandle should warn if used to find a host component inside StrictMode', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -635,6 +702,7 @@ describe('ReactNative', () => { expect(match).toBe(child._nativeTag); }); + // @gate !disableLegacyMode it('findNodeHandle should warn if passed a component that is inside StrictMode', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -670,6 +738,7 @@ describe('ReactNative', () => { expect(match).toBe(child._nativeTag); }); + // @gate !disableLegacyMode it('blur on host component calls TextInputState', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, @@ -687,6 +756,7 @@ describe('ReactNative', () => { expect(TextInputState.blurTextInput).toHaveBeenCalledWith(viewRef.current); }); + // @gate !disableLegacyMode it('focus on host component calls TextInputState', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, diff --git a/packages/react-native-renderer/src/__tests__/__snapshots__/ReactNativeEvents-test.internal.js.snap b/packages/react-native-renderer/src/__tests__/__snapshots__/ReactNativeEvents-test.internal.js.snap deleted file mode 100644 index 7ad7a0ad6f6c0..0000000000000 --- a/packages/react-native-renderer/src/__tests__/__snapshots__/ReactNativeEvents-test.internal.js.snap +++ /dev/null @@ -1,12 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`fails if unknown/unsupported event types are dispatched 1`] = ` -" {} - View null" -`; - -exports[`handles events 1`] = ` -" {} - View {"foo":"outer"} - View {"foo":"inner"}" -`; diff --git a/packages/react-native-renderer/src/__tests__/__snapshots__/ReactNativeMount-test.internal.js.snap b/packages/react-native-renderer/src/__tests__/__snapshots__/ReactNativeMount-test.internal.js.snap deleted file mode 100644 index 0bd001c8f4b08..0000000000000 --- a/packages/react-native-renderer/src/__tests__/__snapshots__/ReactNativeMount-test.internal.js.snap +++ /dev/null @@ -1,51 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ReactNative renders and reorders children 1`] = ` -" {} - RCTView null - RCTView {"title":"a"} - RCTView {"title":"b"} - RCTView {"title":"c"} - RCTView {"title":"d"} - RCTView {"title":"e"} - RCTView {"title":"f"} - RCTView {"title":"g"} - RCTView {"title":"h"} - RCTView {"title":"i"} - RCTView {"title":"j"} - RCTView {"title":"k"} - RCTView {"title":"l"} - RCTView {"title":"m"} - RCTView {"title":"n"} - RCTView {"title":"o"} - RCTView {"title":"p"} - RCTView {"title":"q"} - RCTView {"title":"r"} - RCTView {"title":"s"} - RCTView {"title":"t"}" -`; - -exports[`ReactNative renders and reorders children 2`] = ` -" {} - RCTView null - RCTView {"title":"m"} - RCTView {"title":"x"} - RCTView {"title":"h"} - RCTView {"title":"p"} - RCTView {"title":"g"} - RCTView {"title":"w"} - RCTView {"title":"f"} - RCTView {"title":"r"} - RCTView {"title":"a"} - RCTView {"title":"l"} - RCTView {"title":"k"} - RCTView {"title":"e"} - RCTView {"title":"o"} - RCTView {"title":"i"} - RCTView {"title":"v"} - RCTView {"title":"c"} - RCTView {"title":"s"} - RCTView {"title":"t"} - RCTView {"title":"z"} - RCTView {"title":"y"}" -`; diff --git a/packages/react-native-renderer/src/__tests__/createReactNativeComponentClass-test.internal.js b/packages/react-native-renderer/src/__tests__/createReactNativeComponentClass-test.internal.js index 5bb4b1fba0655..82199fd79e641 100644 --- a/packages/react-native-renderer/src/__tests__/createReactNativeComponentClass-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/createReactNativeComponentClass-test.internal.js @@ -25,6 +25,7 @@ describe('createReactNativeComponentClass', () => { ReactNative = require('react-native-renderer'); }); + // @gate !disableLegacyMode it('should register viewConfigs', () => { const textViewConfig = { validAttributes: {}, diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index af4612f00ddee..03937ee02cfcb 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -32,7 +32,7 @@ import { ConcurrentRoot, LegacyRoot, } from 'react-reconciler/constants'; -import {enableRefAsProp} from 'shared/ReactFeatureFlags'; +import {enableRefAsProp, disableLegacyMode} from 'shared/ReactFeatureFlags'; type Container = { rootID: string, @@ -1020,6 +1020,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { }, createLegacyRoot() { + if (disableLegacyMode) { + throw new Error('createLegacyRoot: Unsupported Legacy Mode API.'); + } + const container = { rootID: '' + idCounter++, pendingChildren: [], @@ -1119,6 +1123,9 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { }, renderLegacySyncRoot(element: React$Element, callback: ?Function) { + if (disableLegacyMode) { + throw new Error('createLegacyRoot: Unsupported Legacy Mode API.'); + } const rootID = DEFAULT_ROOT_ID; const container = ReactNoop.getOrCreateRootContainer(rootID, LegacyRoot); const root = roots.get(container.rootID); diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index d676966907826..7b1a6514ebd5f 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -37,6 +37,7 @@ import { enableDebugTracing, enableDO_NOT_USE_disableStrictPassiveEffect, enableRenderableContext, + disableLegacyMode, } from 'shared/ReactFeatureFlags'; import {NoFlags, Placement, StaticMask} from './ReactFiberFlags'; import {ConcurrentRoot} from './ReactRootTags'; @@ -439,7 +440,7 @@ export function createHostRootFiber( concurrentUpdatesByDefaultOverride: null | boolean, ): Fiber { let mode; - if (tag === ConcurrentRoot) { + if (disableLegacyMode || tag === ConcurrentRoot) { mode = ConcurrentMode; if (isStrictMode === true) { mode |= StrictLegacyMode | StrictEffectsMode; @@ -517,7 +518,7 @@ export function createFiberFromTypeAndProps( case REACT_STRICT_MODE_TYPE: fiberTag = Mode; mode |= StrictLegacyMode; - if ((mode & ConcurrentMode) !== NoMode) { + if (disableLegacyMode || (mode & ConcurrentMode) !== NoMode) { // Strict effects should never run on legacy roots mode |= StrictEffectsMode; if ( diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index fbf7988cdea78..bd745e02d12a2 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -108,6 +108,7 @@ import { enablePostpone, enableRenderableContext, enableRefAsProp, + disableLegacyMode, } from 'shared/ReactFeatureFlags'; import isArray from 'shared/isArray'; import shallowEqual from 'shared/shallowEqual'; @@ -700,7 +701,10 @@ function updateOffscreenComponent( ); } - if ((workInProgress.mode & ConcurrentMode) === NoMode) { + if ( + !disableLegacyMode && + (workInProgress.mode & ConcurrentMode) === NoMode + ) { // In legacy sync mode, don't defer the subtree. Render it now. // TODO: Consider how Offscreen should work with transitions in the future const nextState: OffscreenState = { @@ -2347,6 +2351,7 @@ function mountSuspenseFallbackChildren( let primaryChildFragment; let fallbackChildFragment; if ( + !disableLegacyMode && (mode & ConcurrentMode) === NoMode && progressedPrimaryFragment !== null ) { @@ -2430,7 +2435,7 @@ function updateSuspensePrimaryChildren( children: primaryChildren, }, ); - if ((workInProgress.mode & ConcurrentMode) === NoMode) { + if (!disableLegacyMode && (workInProgress.mode & ConcurrentMode) === NoMode) { primaryChildFragment.lanes = renderLanes; } primaryChildFragment.return = workInProgress; @@ -2471,6 +2476,7 @@ function updateSuspenseFallbackChildren( if ( // In legacy mode, we commit the primary tree as if it successfully // completed, even though it's in an inconsistent state. + !disableLegacyMode && (mode & ConcurrentMode) === NoMode && // Make sure we're on the second pass, i.e. the primary child fragment was // already cloned. In legacy mode, the only case where this isn't true is @@ -2607,7 +2613,7 @@ function mountSuspenseFallbackAfterRetryWithoutHydrating( primaryChildFragment.sibling = fallbackChildFragment; workInProgress.child = primaryChildFragment; - if ((workInProgress.mode & ConcurrentMode) !== NoMode) { + if (disableLegacyMode || (workInProgress.mode & ConcurrentMode) !== NoMode) { // We will have dropped the effect list which contains the // deletion. We need to reconcile to delete the current child. reconcileChildFibers(workInProgress, current.child, null, renderLanes); @@ -3195,7 +3201,7 @@ function updateSuspenseListComponent( } pushSuspenseListContext(workInProgress, suspenseContext); - if ((workInProgress.mode & ConcurrentMode) === NoMode) { + if (!disableLegacyMode && (workInProgress.mode & ConcurrentMode) === NoMode) { // In legacy mode, SuspenseList doesn't work so we just // use make it a noop by treating it as the default revealOrder. workInProgress.memoizedState = null; @@ -3443,7 +3449,7 @@ function resetSuspendedCurrentOnMountInLegacyMode( current: null | Fiber, workInProgress: Fiber, ) { - if ((workInProgress.mode & ConcurrentMode) === NoMode) { + if (!disableLegacyMode && (workInProgress.mode & ConcurrentMode) === NoMode) { if (current !== null) { // A lazy component only mounts if it suspended inside a non- // concurrent tree, in an inconsistent state. We want to treat it like @@ -4013,6 +4019,9 @@ function beginWork( ); } case IncompleteClassComponent: { + if (disableLegacyMode) { + break; + } const Component = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; const resolvedProps = @@ -4028,6 +4037,9 @@ function beginWork( ); } case IncompleteFunctionComponent: { + if (disableLegacyMode) { + break; + } const Component = workInProgress.type; const unresolvedProps = workInProgress.pendingProps; const resolvedProps = diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 7b88509b65d65..ba11d5b6149b2 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -54,6 +54,7 @@ import { enableUseEffectEventHook, enableLegacyHidden, disableStringRefs, + disableLegacyMode, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -1164,7 +1165,8 @@ function commitLayoutEffectOnFiber( break; } case OffscreenComponent: { - const isModernRoot = (finishedWork.mode & ConcurrentMode) !== NoMode; + const isModernRoot = + disableLegacyMode || (finishedWork.mode & ConcurrentMode) !== NoMode; if (isModernRoot) { const isHidden = finishedWork.memoizedState !== null; const newOffscreenSubtreeIsHidden = @@ -2255,7 +2257,7 @@ function commitDeletionEffectsOnFiber( } case OffscreenComponent: { safelyDetachRef(deletedFiber, nearestMountedAncestor); - if (deletedFiber.mode & ConcurrentMode) { + if (disableLegacyMode || deletedFiber.mode & ConcurrentMode) { // If this offscreen component is hidden, we already unmounted it. Before // deleting the children, track that it's already unmounted so that we // don't attempt to unmount the effects again. @@ -2932,7 +2934,7 @@ function commitMutationEffectsOnFiber( const isHidden = newState !== null; const wasHidden = current !== null && current.memoizedState !== null; - if (finishedWork.mode & ConcurrentMode) { + if (disableLegacyMode || finishedWork.mode & ConcurrentMode) { // Before committing the children, track on the stack whether this // offscreen subtree was already hidden, so that we don't unmount the // effects again. @@ -2978,7 +2980,10 @@ function commitMutationEffectsOnFiber( // - This Offscreen was not hidden before. // - Ancestor Offscreen was not hidden in previous commit. if (isUpdate && !wasHidden && !wasHiddenByAncestorOffscreen) { - if ((finishedWork.mode & ConcurrentMode) !== NoMode) { + if ( + disableLegacyMode || + (finishedWork.mode & ConcurrentMode) !== NoMode + ) { // Disappear the layout effects of all the children recursivelyTraverseDisappearLayoutEffects(finishedWork); } @@ -3676,7 +3681,7 @@ function commitPassiveMountOnFiber( committedTransitions, ); } else { - if (finishedWork.mode & ConcurrentMode) { + if (disableLegacyMode || finishedWork.mode & ConcurrentMode) { // The effects are currently disconnected. Since the tree is hidden, // don't connect them. This also applies to the initial render. if (enableCache || enableTransitionTracing) { @@ -3874,7 +3879,7 @@ export function reconnectPassiveEffects( includeWorkInProgressEffects, ); } else { - if (finishedWork.mode & ConcurrentMode) { + if (disableLegacyMode || finishedWork.mode & ConcurrentMode) { // The effects are currently disconnected. Since the tree is hidden, // don't connect them. This also applies to the initial render. if (enableCache || enableTransitionTracing) { diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 01087e3696279..51e21b8d2f480 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -40,6 +40,7 @@ import { enableTransitionTracing, enableRenderableContext, passChildrenWhenCloningPersistedNodes, + disableLegacyMode, } from 'shared/ReactFeatureFlags'; import {now} from './Scheduler'; @@ -949,10 +950,15 @@ function completeWork( // for hydration. popTreeContext(workInProgress); switch (workInProgress.tag) { + case IncompleteFunctionComponent: { + if (disableLegacyMode) { + break; + } + // Fallthrough + } case LazyComponent: case SimpleMemoComponent: case FunctionComponent: - case IncompleteFunctionComponent: case ForwardRef: case Fragment: case Mode: @@ -1475,6 +1481,9 @@ function completeWork( bubbleProperties(workInProgress); return null; case IncompleteClassComponent: { + if (disableLegacyMode) { + break; + } // Same as class component case. I put it down here so that the tags are // sequential to ensure this switch is compiled to a jump table. const Component = workInProgress.type; @@ -1740,7 +1749,11 @@ function completeWork( } } - if (!nextIsHidden || (workInProgress.mode & ConcurrentMode) === NoMode) { + if ( + !nextIsHidden || + (!disableLegacyMode && + (workInProgress.mode & ConcurrentMode) === NoMode) + ) { bubbleProperties(workInProgress); } else { // Don't bubble properties for hidden children unless we're rendering diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 127656768cf30..b548948344d14 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -42,6 +42,7 @@ import { debugRenderPhaseSideEffectsForStrictMode, enableAsyncActions, enableUseDeferredValueInitialArg, + disableLegacyMode, } from 'shared/ReactFeatureFlags'; import { REACT_CONTEXT_TYPE, @@ -662,7 +663,7 @@ function finishRenderingHooks( // need to mark fibers that commit in an incomplete state, somehow. For // now I'll disable the warning that most of the bugs that would trigger // it are either exclusive to concurrent mode or exist in both. - (current.mode & ConcurrentMode) !== NoMode + (disableLegacyMode || (current.mode & ConcurrentMode) !== NoMode) ) { console.error( 'Internal React error: Expected static flag was missing. Please ' + diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index ce63425a542f6..e69989e61624d 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -33,6 +33,7 @@ import { enableProfilerTimer, enableUpdaterTracking, enableTransitionTracing, + disableLegacyMode, } from 'shared/ReactFeatureFlags'; import {initializeUpdateQueue} from './ReactFiberClassUpdateQueue'; import {LegacyRoot, ConcurrentRoot} from './ReactRootTags'; @@ -56,7 +57,7 @@ function FiberRootNode( onRecoverableError: any, formState: ReactFormState | null, ) { - this.tag = tag; + this.tag = disableLegacyMode ? ConcurrentRoot : tag; this.containerInfo = containerInfo; this.pendingChildren = null; this.current = null; @@ -123,13 +124,18 @@ function FiberRootNode( } if (__DEV__) { - switch (tag) { - case ConcurrentRoot: - this._debugRootType = hydrate ? 'hydrateRoot()' : 'createRoot()'; - break; - case LegacyRoot: - this._debugRootType = hydrate ? 'hydrate()' : 'render()'; - break; + if (disableLegacyMode) { + // TODO: This varies by each renderer. + this._debugRootType = hydrate ? 'hydrateRoot()' : 'createRoot()'; + } else { + switch (tag) { + case ConcurrentRoot: + this._debugRootType = hydrate ? 'hydrateRoot()' : 'createRoot()'; + break; + case LegacyRoot: + this._debugRootType = hydrate ? 'hydrate()' : 'render()'; + break; + } } } } diff --git a/packages/react-reconciler/src/ReactFiberRootScheduler.js b/packages/react-reconciler/src/ReactFiberRootScheduler.js index d03131ce1751a..12f7660596664 100644 --- a/packages/react-reconciler/src/ReactFiberRootScheduler.js +++ b/packages/react-reconciler/src/ReactFiberRootScheduler.js @@ -12,7 +12,10 @@ import type {Lane} from './ReactFiberLane'; import type {PriorityLevel} from 'scheduler/src/SchedulerPriorities'; import type {BatchConfigTransition} from './ReactFiberTracingMarkerComponent'; -import {enableDeferRootSchedulingToMicrotask} from 'shared/ReactFeatureFlags'; +import { + disableLegacyMode, + enableDeferRootSchedulingToMicrotask, +} from 'shared/ReactFeatureFlags'; import { NoLane, NoLanes, @@ -131,6 +134,7 @@ export function ensureRootIsScheduled(root: FiberRoot): void { if ( __DEV__ && + !disableLegacyMode && ReactCurrentActQueue.isBatchingLegacy && root.tag === LegacyRoot ) { @@ -148,7 +152,9 @@ export function flushSyncWorkOnAllRoots() { export function flushSyncWorkOnLegacyRootsOnly() { // This is allowed to be called synchronously, but the caller should check // the execution context first. - flushSyncWorkAcrossRoots_impl(true); + if (!disableLegacyMode) { + flushSyncWorkAcrossRoots_impl(true); + } } function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) { @@ -171,7 +177,7 @@ function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) { didPerformSomeWork = false; let root = firstScheduledRoot; while (root !== null) { - if (onlyLegacy && root.tag !== LegacyRoot) { + if (onlyLegacy && (disableLegacyMode || root.tag !== LegacyRoot)) { // Skip non-legacy roots. } else { const workInProgressRoot = getWorkInProgressRoot(); diff --git a/packages/react-reconciler/src/ReactFiberThrow.js b/packages/react-reconciler/src/ReactFiberThrow.js index 5b40a872b1f85..fbad2f2acfe6c 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.js +++ b/packages/react-reconciler/src/ReactFiberThrow.js @@ -43,6 +43,7 @@ import { enableLazyContextPropagation, enableUpdaterTracking, enablePostpone, + disableLegacyMode, } from 'shared/ReactFeatureFlags'; import {createCapturedValueAtFiber} from './ReactCapturedValue'; import { @@ -189,6 +190,7 @@ function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) { // A legacy mode Suspense quirk, only relevant to hook components. const tag = sourceFiber.tag; if ( + !disableLegacyMode && (sourceFiber.mode & ConcurrentMode) === NoMode && (tag === FunctionComponent || tag === ForwardRef || @@ -215,7 +217,10 @@ function markSuspenseBoundaryShouldCapture( ): Fiber | null { // This marks a Suspense boundary so that when we're unwinding the stack, // it captures the suspended "exception" and does a second (fallback) pass. - if ((suspenseBoundary.mode & ConcurrentMode) === NoMode) { + if ( + !disableLegacyMode && + (suspenseBoundary.mode & ConcurrentMode) === NoMode + ) { // Legacy Mode Suspense // // If the boundary is in legacy mode, we should *not* @@ -354,7 +359,10 @@ function throwException( resetSuspendedComponent(sourceFiber, rootRenderLanes); if (__DEV__) { - if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) { + if ( + getIsHydrating() && + (disableLegacyMode || sourceFiber.mode & ConcurrentMode) + ) { markDidThrowWhileHydratingDEV(); } } @@ -383,7 +391,7 @@ function throwException( // we don't have to recompute it on demand. This would also allow us // to unify with `use` which needs to perform this logic even sooner, // before `throwException` is called. - if (sourceFiber.mode & ConcurrentMode) { + if (disableLegacyMode || sourceFiber.mode & ConcurrentMode) { if (getShellBoundary() === null) { // Suspended in the "shell" of the app. This is an undesirable // loading state. We should avoid committing this tree. @@ -451,14 +459,14 @@ function throwException( // We only attach ping listeners in concurrent mode. Legacy // Suspense always commits fallbacks synchronously, so there are // no pings. - if (suspenseBoundary.mode & ConcurrentMode) { + if (disableLegacyMode || suspenseBoundary.mode & ConcurrentMode) { attachPingListener(root, wakeable, rootRenderLanes); } } return false; } case OffscreenComponent: { - if (suspenseBoundary.mode & ConcurrentMode) { + if (disableLegacyMode || suspenseBoundary.mode & ConcurrentMode) { suspenseBoundary.flags |= ShouldCapture; const isSuspenseyResource = wakeable === noopSuspenseyCommitThenable; @@ -497,7 +505,7 @@ function throwException( // No boundary was found. Unless this is a sync update, this is OK. // We can suspend and wait for more data to arrive. - if (root.tag === ConcurrentRoot) { + if (disableLegacyMode || root.tag === ConcurrentRoot) { // In a concurrent root, suspending without a Suspense boundary is // allowed. It will suspend indefinitely without committing. // @@ -522,7 +530,10 @@ function throwException( } // This is a regular error, not a Suspense wakeable. - if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) { + if ( + getIsHydrating() && + (disableLegacyMode || sourceFiber.mode & ConcurrentMode) + ) { markDidThrowWhileHydratingDEV(); const suspenseBoundary = getSuspenseHandler(); // If the error was thrown during hydration, we may be able to recover by diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 2570f857c7d2d..36182d999403b 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -602,7 +602,7 @@ export function getCurrentTime(): number { export function requestUpdateLane(fiber: Fiber): Lane { // Special cases const mode = fiber.mode; - if ((mode & ConcurrentMode) === NoMode) { + if (!disableLegacyMode && (mode & ConcurrentMode) === NoMode) { return (SyncLane: Lane); } else if ( (executionContext & RenderContext) !== NoContext && @@ -669,7 +669,7 @@ function requestRetryLane(fiber: Fiber) { // Special cases const mode = fiber.mode; - if ((mode & ConcurrentMode) === NoMode) { + if (!disableLegacyMode && (mode & ConcurrentMode) === NoMode) { return (SyncLane: Lane); } @@ -824,6 +824,7 @@ export function scheduleUpdateOnFiber( if ( lane === SyncLane && executionContext === NoContext && + !disableLegacyMode && (fiber.mode & ConcurrentMode) === NoMode ) { if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy) { @@ -1367,7 +1368,10 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null { } let exitStatus = renderRootSync(root, lanes); - if (root.tag !== LegacyRoot && exitStatus === RootErrored) { + if ( + (disableLegacyMode || root.tag !== LegacyRoot) && + exitStatus === RootErrored + ) { // If something threw an error, try rendering one more time. We'll render // synchronously to block concurrent data mutations, and we'll includes // all pending updates are included. If it still fails after the second @@ -1515,6 +1519,7 @@ export function flushSync(fn: (() => R) | void): R | void { // next event, not at the end of the previous one. if ( rootWithPendingPassiveEffects !== null && + !disableLegacyMode && rootWithPendingPassiveEffects.tag === LegacyRoot && (executionContext & (RenderContext | CommitContext)) === NoContext ) { @@ -3035,7 +3040,10 @@ function commitRootImpl( // TODO: We can optimize this by not scheduling the callback earlier. Since we // currently schedule the callback in multiple places, will wait until those // are consolidated. - if (includesSyncLane(pendingPassiveEffectsLanes) && root.tag !== LegacyRoot) { + if ( + includesSyncLane(pendingPassiveEffectsLanes) && + (disableLegacyMode || root.tag !== LegacyRoot) + ) { flushPassiveEffects(); } @@ -3716,11 +3724,11 @@ function commitDoubleInvokeEffectsInDEV( hasPassiveEffects: boolean, ) { if (__DEV__) { - if (useModernStrictMode && root.tag !== LegacyRoot) { + if (useModernStrictMode && (disableLegacyMode || root.tag !== LegacyRoot)) { let doubleInvokeEffects = true; if ( - root.tag === ConcurrentRoot && + (disableLegacyMode || root.tag === ConcurrentRoot) && !(root.current.mode & (StrictLegacyMode | StrictEffectsMode)) ) { doubleInvokeEffects = false; @@ -3794,7 +3802,7 @@ export function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber: Fiber) { return; } - if (!(fiber.mode & ConcurrentMode)) { + if (!disableLegacyMode && !(fiber.mode & ConcurrentMode)) { return; } @@ -3933,7 +3941,7 @@ function shouldForceFlushFallbacksInDEV() { function warnIfUpdatesNotWrappedWithActDEV(fiber: Fiber): void { if (__DEV__) { - if (fiber.mode & ConcurrentMode) { + if (disableLegacyMode || fiber.mode & ConcurrentMode) { if (!isConcurrentActEnvironment()) { // Not in an act environment. No need to warn. return; @@ -3991,7 +3999,7 @@ function warnIfUpdatesNotWrappedWithActDEV(fiber: Fiber): void { function warnIfSuspenseResolutionNotWrappedWithActDEV(root: FiberRoot): void { if (__DEV__) { if ( - root.tag !== LegacyRoot && + (disableLegacyMode || root.tag !== LegacyRoot) && isConcurrentActEnvironment() && ReactCurrentActQueue.current === null ) { diff --git a/packages/react-reconciler/src/__tests__/Activity-test.js b/packages/react-reconciler/src/__tests__/Activity-test.js index fc6a8b563d197..d37513e01eb44 100644 --- a/packages/react-reconciler/src/__tests__/Activity-test.js +++ b/packages/react-reconciler/src/__tests__/Activity-test.js @@ -118,7 +118,7 @@ describe('Activity', () => { ); }); - // @gate www + // @gate www && !disableLegacyMode it('does not defer in legacy mode', async () => { let setState; function Foo() { diff --git a/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js b/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js index e4e100fe2daf9..f2124f9f99875 100644 --- a/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js +++ b/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js @@ -76,7 +76,7 @@ describe('DebugTracing', () => { expect(logs).toEqual([]); }); - // @gate experimental && build === 'development' && enableDebugTracing + // @gate experimental && build === 'development' && enableDebugTracing && !disableLegacyMode it('should log sync render with suspense, legacy', async () => { let resolveFakeSuspensePromise; let didResolve = false; @@ -116,7 +116,7 @@ describe('DebugTracing', () => { expect(logs).toEqual(['log: ⚛️ Example resolved']); }); - // @gate experimental && build === 'development' && enableDebugTracing && enableCPUSuspense + // @gate experimental && build === 'development' && enableDebugTracing && enableCPUSuspense && !disableLegacyMode it('should log sync render with CPU suspense, legacy', async () => { function Example() { console.log(''); diff --git a/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js b/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js index 3247e8758e079..20e102ed6f718 100644 --- a/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js +++ b/packages/react-reconciler/src/__tests__/ReactContextPropagation-test.js @@ -489,7 +489,7 @@ describe('ReactLazyContextPropagation', () => { expect(root).toMatchRenderedOutput('BBB'); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode test('context is propagated across retries (legacy)', async () => { const root = ReactNoop.createLegacyRoot(); diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js index 04e7be86c61cf..ef73e5743957b 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js @@ -1622,6 +1622,7 @@ describe('ReactHooksWithNoopRenderer', () => { expect(ReactNoop).toMatchRenderedOutput(); }); + // @gate !disableLegacyMode it( 'in legacy mode, useEffect is deferred and updates finish synchronously ' + '(in a single batch)', diff --git a/packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js b/packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js index 6b1c284a7eb9d..7fbb3efc3bb62 100644 --- a/packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIsomorphicAct-test.js @@ -108,7 +108,7 @@ describe('isomorphic act()', () => { expect(returnValue).toEqual('hi'); }); - // @gate __DEV__ + // @gate __DEV__ && !disableLegacyMode test('in legacy mode, updates are batched', () => { const root = ReactNoop.createLegacyRoot(); @@ -136,7 +136,7 @@ describe('isomorphic act()', () => { expect(root).toMatchRenderedOutput('C'); }); - // @gate __DEV__ + // @gate __DEV__ && !disableLegacyMode test('in legacy mode, in an async scope, updates are batched until the first `await`', async () => { const root = ReactNoop.createLegacyRoot(); @@ -167,7 +167,7 @@ describe('isomorphic act()', () => { }); }); - // @gate __DEV__ + // @gate __DEV__ && !disableLegacyMode test('in legacy mode, in an async scope, updates are batched until the first `await` (regression test: batchedUpdates)', async () => { const root = ReactNoop.createLegacyRoot(); diff --git a/packages/react-reconciler/src/__tests__/ReactSubtreeFlagsWarning-test.js b/packages/react-reconciler/src/__tests__/ReactSubtreeFlagsWarning-test.js index e5d9c9c445dad..24c4266b50f2d 100644 --- a/packages/react-reconciler/src/__tests__/ReactSubtreeFlagsWarning-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSubtreeFlagsWarning-test.js @@ -130,7 +130,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { const resolveText = resolveMostRecentTextCache; - // @gate experimental || www + // @gate www && !disableLegacyMode it('regression: false positive for legacy suspense', async () => { const Child = ({text}) => { // If text hasn't resolved, this will throw and exit before the passive diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js index 85a983a9161dd..e0137575164b0 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js @@ -324,7 +324,7 @@ describe('ReactSuspenseEffectsSemantics', () => { expect(ReactNoop).toMatchRenderedOutput(null); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode it('should not change behavior in sync', async () => { class ClassText extends React.Component { componentDidMount() { @@ -445,7 +445,7 @@ describe('ReactSuspenseEffectsSemantics', () => { }); describe('layout effects within a tree that re-suspends in an update', () => { - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode it('should not be destroyed or recreated in legacy roots', async () => { function App({children = null}) { Scheduler.log('App render'); @@ -2542,7 +2542,7 @@ describe('ReactSuspenseEffectsSemantics', () => { return null; } - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode it('should not be cleared within legacy roots', async () => { class ClassComponent extends React.Component { render() { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js index d099b6d72ffb2..9e16297c5cd90 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js @@ -4,6 +4,7 @@ let ReactNoop; let Scheduler; let act; let Random; +let ReactFeatureFlags; const SEED = process.env.FUZZ_TEST_SEED || 'default'; const prettyFormatPkg = require('pretty-format'); @@ -26,6 +27,7 @@ describe('ReactSuspenseFuzz', () => { Scheduler = require('scheduler'); act = require('internal-test-utils').act; Random = require('random-seed'); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); }); jest.setTimeout(20000); @@ -163,16 +165,21 @@ describe('ReactSuspenseFuzz', () => { resetCache(); // Do it again in legacy mode. - const legacyRootThatSuspends = ReactNoop.createLegacyRoot(); - await act(() => { - legacyRootThatSuspends.render(children); - }); + if (!ReactFeatureFlags.disableLegacyMode) { + const legacyRootThatSuspends = ReactNoop.createLegacyRoot(); + await act(() => { + legacyRootThatSuspends.render(children); + }); + + expect(legacyRootThatSuspends.getChildrenAsJSX()).toEqual( + expectedOutput, + ); + } // Now compare the final output. It should be the same. expect(concurrentRootThatSuspends.getChildrenAsJSX()).toEqual( expectedOutput, ); - expect(legacyRootThatSuspends.getChildrenAsJSX()).toEqual(expectedOutput); // TODO: There are Scheduler logs in this test file but they were only // added for debugging purposes; we don't make any assertions on them. diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js index 432546c683f5b..e69d7e4123425 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js @@ -272,7 +272,7 @@ describe('ReactSuspenseList', () => { ); }); - // @gate enableSuspenseList + // @gate enableSuspenseList && !disableLegacyMode it('shows content independently in legacy mode regardless of option', async () => { const A = createAsyncText('A'); const B = createAsyncText('B'); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js index a1771263fbd7c..01e2323df95e7 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js @@ -296,6 +296,7 @@ describe('ReactSuspensePlaceholder', () => { }); describe('when suspending during mount', () => { + // @gate !disableLegacyMode && !disableLegacyMode it('properly accounts for base durations when a suspended times out in a legacy tree', async () => { ReactNoop.renderLegacySyncRoot(); assertLog([ @@ -370,6 +371,7 @@ describe('ReactSuspensePlaceholder', () => { }); describe('when suspending during update', () => { + // @gate !disableLegacyMode && !disableLegacyMode it('properly accounts for base durations when a suspended times out in a legacy tree', async () => { ReactNoop.renderLegacySyncRoot( , diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js index 2d75f2bda18b0..fd37cdac69934 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js @@ -886,7 +886,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { }).not.toThrow(); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode it('in legacy mode, errors when an update suspends without a Suspense boundary during a sync update', async () => { const root = ReactNoop.createLegacyRoot(); await expect(async () => { @@ -1032,7 +1032,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { }); describe('legacy mode mode', () => { - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode it('times out immediately', async () => { function App() { return ( @@ -1055,7 +1055,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { expect(ReactNoop).toMatchRenderedOutput(); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode it('times out immediately when Suspense is in legacy mode', async () => { class UpdatingText extends React.Component { state = {step: 1}; @@ -1129,7 +1129,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { ); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode it('does not re-render siblings in loose mode', async () => { class TextWithLifecycle extends React.Component { componentDidMount() { @@ -1204,7 +1204,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { ); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode it('suspends inside constructor', async () => { class AsyncTextInConstructor extends React.Component { constructor(props) { @@ -1240,7 +1240,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { expect(ReactNoop).toMatchRenderedOutput(); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode it('does not infinite loop if fallback contains lifecycle method', async () => { class Fallback extends React.Component { state = { @@ -1283,7 +1283,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { }); if (global.__PERSISTENT__) { - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode it('hides/unhides suspended children before layout effects fire (persistent)', async () => { const {useRef, useLayoutEffect} = React; @@ -1327,7 +1327,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { assertLog(['Hi']); }); } else { - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode it('hides/unhides suspended children before layout effects fire (mutation)', async () => { const {useRef, useLayoutEffect} = React; @@ -1370,7 +1370,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { }); } - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode it('handles errors in the return path of a component that suspends', async () => { // Covers an edge case where an error is thrown inside the complete phase // of a component that is in the return path of a component that suspends. @@ -1405,6 +1405,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { ); }); + // @gate !disableLegacyMode it('does not drop mounted effects', async () => { const never = {then() {}}; @@ -1455,7 +1456,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { }); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode it('does not call lifecycles of a suspended component', async () => { class TextWithLifecycle extends React.Component { componentDidMount() { @@ -1523,7 +1524,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { ); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode it('does not call lifecycles of a suspended component (hooks)', async () => { function TextWithLifecycle(props) { React.useLayoutEffect(() => { @@ -3885,7 +3886,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { assertLog(['Unmount Child']); }); - // @gate enableLegacyCache + // @gate enableLegacyCache && !disableLegacyMode it('should fire effect clean-up when deleting suspended tree (legacy)', async () => { const {useEffect} = React; diff --git a/packages/react-reconciler/src/__tests__/StrictEffectsMode-test.js b/packages/react-reconciler/src/__tests__/StrictEffectsMode-test.js index f673bf01c2cc0..34ee0b1851613 100644 --- a/packages/react-reconciler/src/__tests__/StrictEffectsMode-test.js +++ b/packages/react-reconciler/src/__tests__/StrictEffectsMode-test.js @@ -27,6 +27,7 @@ describe('StrictEffectsMode', () => { ReactNoop = require('react-noop-renderer'); }); + // @gate !disableLegacyMode it('should not double invoke effects in legacy mode', async () => { function App({text}) { React.useEffect(() => { @@ -430,6 +431,7 @@ describe('StrictEffectsMode', () => { assertLog(['componentWillUnmount']); }); + // @gate !disableLegacyMode it('should not double invoke class lifecycles in legacy mode', async () => { class App extends React.PureComponent { componentDidMount() { diff --git a/packages/react-reconciler/src/__tests__/StrictEffectsModeDefaults-test.internal.js b/packages/react-reconciler/src/__tests__/StrictEffectsModeDefaults-test.internal.js index 880b77a27abe7..ef0f494b687da 100644 --- a/packages/react-reconciler/src/__tests__/StrictEffectsModeDefaults-test.internal.js +++ b/packages/react-reconciler/src/__tests__/StrictEffectsModeDefaults-test.internal.js @@ -34,6 +34,7 @@ describe('StrictEffectsMode defaults', () => { assertLog = InternalTestUtils.assertLog; }); + // @gate !disableLegacyMode it('should not double invoke effects in legacy mode', async () => { function App({text}) { React.useEffect(() => { @@ -60,6 +61,7 @@ describe('StrictEffectsMode defaults', () => { assertLog(['useLayoutEffect mount', 'useEffect mount']); }); + // @gate !disableLegacyMode it('should not double invoke class lifecycles in legacy mode', async () => { class App extends React.PureComponent { componentDidMount() { diff --git a/packages/react-reconciler/src/getComponentNameFromFiber.js b/packages/react-reconciler/src/getComponentNameFromFiber.js index 9eb7fdf4f8907..6659b14b7f0d0 100644 --- a/packages/react-reconciler/src/getComponentNameFromFiber.js +++ b/packages/react-reconciler/src/getComponentNameFromFiber.js @@ -11,6 +11,7 @@ import type {ReactContext, ReactConsumerType} from 'shared/ReactTypes'; import type {Fiber} from './ReactInternalTypes'; import { + disableLegacyMode, enableLegacyHidden, enableRenderableContext, } from 'shared/ReactFeatureFlags'; @@ -35,6 +36,7 @@ import { SimpleMemoComponent, LazyComponent, IncompleteClassComponent, + IncompleteFunctionComponent, DehydratedFragment, SuspenseListComponent, ScopeComponent, @@ -123,10 +125,15 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null { return 'SuspenseList'; case TracingMarkerComponent: return 'TracingMarker'; - // The display name for this tags come from the user-provided type: + // The display name for these tags come from the user-provided type: + case IncompleteClassComponent: + case IncompleteFunctionComponent: + if (disableLegacyMode) { + break; + } + // Fallthrough case ClassComponent: case FunctionComponent: - case IncompleteClassComponent: case MemoComponent: case SimpleMemoComponent: if (typeof type === 'function') { diff --git a/packages/react/src/ReactAct.js b/packages/react/src/ReactAct.js index 269c0b2de2eda..6a98a21ac29dd 100644 --- a/packages/react/src/ReactAct.js +++ b/packages/react/src/ReactAct.js @@ -12,6 +12,8 @@ import type {RendererTask} from './ReactCurrentActQueue'; import ReactCurrentActQueue from './ReactCurrentActQueue'; import queueMacrotask from 'shared/enqueueTask'; +import {disableLegacyMode} from 'shared/ReactFeatureFlags'; + // `act` calls can be nested, so we track the depth. This represents the // number of `act` scopes on the stack. let actScopeDepth = 0; @@ -38,7 +40,9 @@ export function act(callback: () => T | Thenable): Thenable { // `act` calls can be nested. // // If we're already inside an `act` scope, reuse the existing queue. - const prevIsBatchingLegacy = ReactCurrentActQueue.isBatchingLegacy; + const prevIsBatchingLegacy = !disableLegacyMode + ? ReactCurrentActQueue.isBatchingLegacy + : false; const prevActQueue = ReactCurrentActQueue.current; const prevActScopeDepth = actScopeDepth; actScopeDepth++; @@ -48,7 +52,9 @@ export function act(callback: () => T | Thenable): Thenable { // set to `true` while the given callback is executed, not for updates // triggered during an async event, because this is how the legacy // implementation of `act` behaved. - ReactCurrentActQueue.isBatchingLegacy = true; + if (!disableLegacyMode) { + ReactCurrentActQueue.isBatchingLegacy = true; + } let result; // This tracks whether the `act` call is awaited. In certain cases, not @@ -58,10 +64,13 @@ export function act(callback: () => T | Thenable): Thenable { // Reset this to `false` right before entering the React work loop. The // only place we ever read this fields is just below, right after running // the callback. So we don't need to reset after the callback runs. - ReactCurrentActQueue.didScheduleLegacyUpdate = false; + if (!disableLegacyMode) { + ReactCurrentActQueue.didScheduleLegacyUpdate = false; + } result = callback(); - const didScheduleLegacyUpdate = - ReactCurrentActQueue.didScheduleLegacyUpdate; + const didScheduleLegacyUpdate = !disableLegacyMode + ? ReactCurrentActQueue.didScheduleLegacyUpdate + : false; // Replicate behavior of original `act` implementation in legacy mode, // which flushed updates immediately after the scope function exits, even @@ -73,7 +82,9 @@ export function act(callback: () => T | Thenable): Thenable { // one used to track `act` scopes. Why, you may be wondering? Because // that's how it worked before version 18. Yes, it's confusing! We should // delete legacy mode!! - ReactCurrentActQueue.isBatchingLegacy = prevIsBatchingLegacy; + if (!disableLegacyMode) { + ReactCurrentActQueue.isBatchingLegacy = prevIsBatchingLegacy; + } } catch (error) { // `isBatchingLegacy` gets reset using the regular stack, not the async // one used to track `act` scopes. Why, you may be wondering? Because @@ -82,7 +93,9 @@ export function act(callback: () => T | Thenable): Thenable { ReactCurrentActQueue.thrownErrors.push(error); } if (ReactCurrentActQueue.thrownErrors.length > 0) { - ReactCurrentActQueue.isBatchingLegacy = prevIsBatchingLegacy; + if (!disableLegacyMode) { + ReactCurrentActQueue.isBatchingLegacy = prevIsBatchingLegacy; + } popActScope(prevActQueue, prevActScopeDepth); const thrownError = aggregateErrors(ReactCurrentActQueue.thrownErrors); ReactCurrentActQueue.thrownErrors.length = 0;