diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 94f5b0cbb0262..247bf7ce8739e 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -87,6 +87,7 @@ import { NoMode, ProfileMode, StrictMode, + BatchedMode, } from './ReactTypeOfMode'; import { shouldSetTextContent, @@ -1493,8 +1494,8 @@ function updateSuspenseComponent( null, ); - if ((workInProgress.mode & ConcurrentMode) === NoMode) { - // Outside of concurrent mode, we commit the effects from the + if ((workInProgress.mode & BatchedMode) === NoMode) { + // Outside of batched mode, we commit the effects from the // partially completed, timed-out tree, too. const progressedState: SuspenseState = workInProgress.memoizedState; const progressedPrimaryChild: Fiber | null = @@ -1546,8 +1547,8 @@ function updateSuspenseComponent( NoWork, ); - if ((workInProgress.mode & ConcurrentMode) === NoMode) { - // Outside of concurrent mode, we commit the effects from the + if ((workInProgress.mode & BatchedMode) === NoMode) { + // Outside of batched mode, we commit the effects from the // partially completed, timed-out tree, too. const progressedState: SuspenseState = workInProgress.memoizedState; const progressedPrimaryChild: Fiber | null = @@ -1629,8 +1630,8 @@ function updateSuspenseComponent( // schedule a placement. // primaryChildFragment.effectTag |= Placement; - if ((workInProgress.mode & ConcurrentMode) === NoMode) { - // Outside of concurrent mode, we commit the effects from the + if ((workInProgress.mode & BatchedMode) === NoMode) { + // Outside of batched mode, we commit the effects from the // partially completed, timed-out tree, too. const progressedState: SuspenseState = workInProgress.memoizedState; const progressedPrimaryChild: Fiber | null = diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index dbd0c7e1743f4..95ed3b9de78bd 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -43,7 +43,7 @@ import { EventComponent, EventTarget, } from 'shared/ReactWorkTags'; -import {ConcurrentMode, NoMode} from './ReactTypeOfMode'; +import {NoMode, BatchedMode} from './ReactTypeOfMode'; import { Placement, Ref, @@ -716,12 +716,12 @@ function completeWork( } if (nextDidTimeout && !prevDidTimeout) { - // If this subtreee is running in concurrent mode we can suspend, + // If this subtreee is running in batched mode we can suspend, // otherwise we won't suspend. // TODO: This will still suspend a synchronous tree if anything // in the concurrent tree already suspended during this render. // This is a known bug. - if ((workInProgress.mode & ConcurrentMode) !== NoMode) { + if ((workInProgress.mode & BatchedMode) !== NoMode) { renderDidSuspend(); } } diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index a39632acf08cb..1a84ba2af2350 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -41,7 +41,7 @@ import { enableSuspenseServerRenderer, enableEventAPI, } from 'shared/ReactFeatureFlags'; -import {ConcurrentMode, NoMode} from './ReactTypeOfMode'; +import {NoMode, BatchedMode} from './ReactTypeOfMode'; import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent'; import {createCapturedValue} from './ReactCapturedValue'; @@ -223,15 +223,15 @@ function throwException( thenables.add(thenable); } - // If the boundary is outside of concurrent mode, we should *not* + // If the boundary is outside of batched mode, we should *not* // suspend the commit. Pretend as if the suspended component rendered // null and keep rendering. In the commit phase, we'll schedule a // subsequent synchronous update to re-render the Suspense. // // Note: It doesn't matter whether the component that suspended was - // inside a concurrent mode tree. If the Suspense is outside of it, we + // inside a batched mode tree. If the Suspense is outside of it, we // should *not* suspend the commit. - if ((workInProgress.mode & ConcurrentMode) === NoMode) { + if ((workInProgress.mode & BatchedMode) === NoMode) { workInProgress.effectTag |= DidCapture; // We're going to commit this fiber even though it didn't complete. diff --git a/packages/react-reconciler/src/__tests__/ReactBatchedMode-test.internal.js b/packages/react-reconciler/src/__tests__/ReactBatchedMode-test.internal.js index ad85a7a4bc980..b270033071228 100644 --- a/packages/react-reconciler/src/__tests__/ReactBatchedMode-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactBatchedMode-test.internal.js @@ -2,6 +2,9 @@ let React; let ReactFeatureFlags; let ReactNoop; let Scheduler; +let ReactCache; +let Suspense; +let TextResource; describe('ReactBatchedMode', () => { beforeEach(() => { @@ -12,6 +15,17 @@ describe('ReactBatchedMode', () => { React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); + ReactCache = require('react-cache'); + Suspense = React.Suspense; + + TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => { + return new Promise((resolve, reject) => + setTimeout(() => { + Scheduler.yieldValue(`Promise resolved [${text}]`); + resolve(text); + }, ms), + ); + }, ([text, ms]) => text); }); function Text(props) { @@ -19,6 +33,22 @@ describe('ReactBatchedMode', () => { return props.text; } + function AsyncText(props) { + const text = props.text; + try { + TextResource.read([props.text, props.ms]); + Scheduler.yieldValue(text); + return props.text; + } catch (promise) { + if (typeof promise.then === 'function') { + Scheduler.yieldValue(`Suspend! [${text}]`); + } else { + Scheduler.yieldValue(`Error! [${text}]`); + } + throw promise; + } + } + it('updates flush without yielding in the next event', () => { const root = ReactNoop.createSyncRoot(); @@ -55,4 +85,38 @@ describe('ReactBatchedMode', () => { expect(Scheduler).toFlushExpired(['Hi', 'Layout effect']); expect(root).toMatchRenderedOutput('Hi'); }); + + it('uses proper Suspense semantics, not legacy ones', async () => { + const root = ReactNoop.createSyncRoot(); + root.render( + }> + + + + + + + + + + , + ); + + expect(Scheduler).toFlushExpired(['A', 'Suspend! [B]', 'C', 'Loading...']); + // In Legacy Mode, A and B would mount in a hidden primary tree. In Batched + // and Concurrent Mode, nothing in the primary tree should mount. But the + // fallback should mount immediately. + expect(root).toMatchRenderedOutput('Loading...'); + + await jest.advanceTimersByTime(1000); + expect(Scheduler).toHaveYielded(['Promise resolved [B]']); + expect(Scheduler).toFlushExpired(['A', 'B', 'C']); + expect(root).toMatchRenderedOutput( + + A + B + C + , + ); + }); }); diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js index a1e12ca463750..83cfd07dc91a1 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js @@ -941,7 +941,7 @@ describe('ReactHooksWithNoopRenderer', () => { }); it( - 'in sync mode, useEffect is deferred and updates finish synchronously ' + + 'in legacy mode, useEffect is deferred and updates finish synchronously ' + '(in a single batch)', () => { function Counter(props) { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js index 5c0323a657494..4bbc8cc370528 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseFuzz-test.internal.js @@ -175,8 +175,8 @@ describe('ReactSuspenseFuzz', () => { resetCache(); ReactNoop.renderLegacySyncRoot(children); resolveAllTasks(); - const syncOutput = ReactNoop.getChildrenAsJSX(); - expect(syncOutput).toEqual(expectedOutput); + const legacyOutput = ReactNoop.getChildrenAsJSX(); + expect(legacyOutput).toEqual(expectedOutput); ReactNoop.renderLegacySyncRoot(null); resetCache(); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js index 7060f427303a1..619ad456732af 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js @@ -303,7 +303,7 @@ describe('ReactSuspensePlaceholder', () => { }); describe('when suspending during mount', () => { - it('properly accounts for base durations when a suspended times out in a sync tree', () => { + it('properly accounts for base durations when a suspended times out in a legacy tree', () => { ReactNoop.renderLegacySyncRoot(); expect(Scheduler).toHaveYielded([ 'App', @@ -373,7 +373,7 @@ describe('ReactSuspensePlaceholder', () => { }); describe('when suspending during update', () => { - it('properly accounts for base durations when a suspended times out in a sync tree', () => { + it('properly accounts for base durations when a suspended times out in a legacy tree', () => { ReactNoop.renderLegacySyncRoot( , ); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index 10764e607cc1d..390b43c7a1331 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -869,7 +869,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { ); }); - describe('sync mode', () => { + describe('legacy mode mode', () => { it('times out immediately', async () => { function App() { return ( @@ -977,7 +977,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { it( 'continues rendering asynchronously even if a promise is captured by ' + - 'a sync boundary (default mode)', + 'a sync boundary (legacy mode)', async () => { class UpdatingText extends React.Component { state = {text: this.props.initialText}; @@ -1109,7 +1109,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { it( 'continues rendering asynchronously even if a promise is captured by ' + - 'a sync boundary (strict, non-concurrent)', + 'a sync boundary (strict, legacy)', async () => { class UpdatingText extends React.Component { state = {text: this.props.initialText};