diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js index 2fcd1830c004..741083a8650c 100644 --- a/packages/react-reconciler/src/ReactFiber.new.js +++ b/packages/react-reconciler/src/ReactFiber.new.js @@ -56,6 +56,7 @@ import { ScopeComponent, Block, OffscreenComponent, + LegacyHiddenComponent, } from './ReactWorkTags'; import getComponentName from 'shared/getComponentName'; @@ -90,6 +91,7 @@ import { REACT_SCOPE_TYPE, REACT_BLOCK_TYPE, REACT_OFFSCREEN_TYPE, + REACT_LEGACY_HIDDEN_TYPE, } from 'shared/ReactSymbols'; export type {Fiber}; @@ -521,6 +523,13 @@ export function createFiberFromTypeAndProps( expirationTime, key, ); + case REACT_LEGACY_HIDDEN_TYPE: + return createFiberFromLegacyHidden( + pendingProps, + mode, + expirationTime, + key, + ); default: { if (typeof type === 'object' && type !== null) { switch (type.$$typeof) { @@ -756,6 +765,24 @@ export function createFiberFromOffscreen( return fiber; } +export function createFiberFromLegacyHidden( + pendingProps: OffscreenProps, + mode: TypeOfMode, + expirationTime: ExpirationTimeOpaque, + key: null | string, +) { + const fiber = createFiber(LegacyHiddenComponent, pendingProps, key, mode); + // TODO: The LegacyHidden fiber shouldn't have a type. It has a tag. + // This needs to be fixed in getComponentName so that it relies on the tag + // instead. + if (__DEV__) { + fiber.type = REACT_LEGACY_HIDDEN_TYPE; + } + fiber.elementType = REACT_LEGACY_HIDDEN_TYPE; + fiber.expirationTime_opaque = expirationTime; + return fiber; +} + export function createFiberFromText( content: string, mode: TypeOfMode, diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 398275087968..576016d2f694 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -50,6 +50,7 @@ import { ScopeComponent, Block, OffscreenComponent, + LegacyHiddenComponent, } from './ReactWorkTags'; import { NoEffect, @@ -79,7 +80,12 @@ import invariant from 'shared/invariant'; import shallowEqual from 'shared/shallowEqual'; import getComponentName from 'shared/getComponentName'; import ReactStrictModeWarnings from './ReactStrictModeWarnings.new'; -import {REACT_LAZY_TYPE, getIteratorFn} from 'shared/ReactSymbols'; +import { + REACT_ELEMENT_TYPE, + REACT_LAZY_TYPE, + REACT_LEGACY_HIDDEN_TYPE, + getIteratorFn, +} from 'shared/ReactSymbols'; import { getCurrentFiberOwnerNameInDevOrNull, setIsRendering, @@ -187,6 +193,7 @@ import { renderDidSuspendDelayIfPossible, markUnprocessedUpdateTime, getWorkInProgressRoot, + pushRenderExpirationTime, } from './ReactFiberWorkLoop.new'; import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev'; @@ -569,15 +576,67 @@ function updateOffscreenComponent( const nextProps: OffscreenProps = workInProgress.pendingProps; const nextChildren = nextProps.children; - if (current !== null) { - if (nextProps.mode === 'hidden') { - // TODO: Should currently be unreachable because Offscreen is only used as - // an implementation detail of Suspense. Once this is a public API, it - // will need to create an OffscreenState. + const prevState: OffscreenState | null = + current !== null ? current.memoizedState : null; + + if (nextProps.mode === 'hidden') { + if ( + !isSameExpirationTime(renderExpirationTime, (Never: ExpirationTimeOpaque)) + ) { + let nextBaseTime; + if (prevState !== null) { + const prevBaseTime = prevState.baseTime; + nextBaseTime = !isSameOrHigherPriority( + prevBaseTime, + renderExpirationTime, + ) + ? prevBaseTime + : renderExpirationTime; + } else { + nextBaseTime = renderExpirationTime; + } + + // Schedule this fiber to re-render at offscreen priority. Then bailout. + if (enableSchedulerTracing) { + markSpawnedWork((Never: ExpirationTimeOpaque)); + } + workInProgress.expirationTime_opaque = workInProgress.childExpirationTime_opaque = Never; + const nextState: OffscreenState = { + baseTime: nextBaseTime, + }; + workInProgress.memoizedState = nextState; + // We're about to bail out, but we need to push this to the stack anyway + // to avoid a push/pop misalignment. + pushRenderExpirationTime(workInProgress, nextBaseTime); + return null; } else { - // Clear the offscreen state. + // Rendering at offscreen, so we can clear the base time. + const nextState: OffscreenState = { + baseTime: NoWork, + }; + workInProgress.memoizedState = nextState; + pushRenderExpirationTime(workInProgress, renderExpirationTime); + } + } else { + let subtreeRenderTime; + if (prevState !== null) { + const baseTime = prevState.baseTime; + subtreeRenderTime = !isSameOrHigherPriority( + baseTime, + renderExpirationTime, + ) + ? baseTime + : renderExpirationTime; + + // Since we're not hidden anymore, reset the state workInProgress.memoizedState = null; + } else { + // We weren't previously hidden, and we still aren't, so there's nothing + // special to do. Need to push to the stack regardless, though, to avoid + // a push/pop misalignment. + subtreeRenderTime = renderExpirationTime; } + pushRenderExpirationTime(workInProgress, subtreeRenderTime); } reconcileChildren( @@ -589,6 +648,11 @@ function updateOffscreenComponent( return workInProgress.child; } +// Note: These happen to have identical begin phases, for now. We shouldn't hold +// ourselves to this constraint, though. If the behavior diverges, we should +// fork the function. +const updateLegacyHiddenComponent = updateOffscreenComponent; + function updateFragment( current: Fiber | null, workInProgress: Fiber, @@ -1138,21 +1202,23 @@ function updateHostComponent( markRef(current, workInProgress); - // Check the host config to see if the children are offscreen/hidden. if ( - workInProgress.mode & ConcurrentMode && - !isSameExpirationTime( - renderExpirationTime, - (Never: ExpirationTimeOpaque), - ) && - shouldDeprioritizeSubtree(type, nextProps) + (workInProgress.mode & ConcurrentMode) !== NoMode && + nextProps.hasOwnProperty('hidden') ) { - if (enableSchedulerTracing) { - markSpawnedWork((Never: ExpirationTimeOpaque)); - } - // Schedule this fiber to re-render at offscreen priority. Then bailout. - workInProgress.expirationTime_opaque = workInProgress.childExpirationTime_opaque = Never; - return null; + const wrappedChildren = { + $$typeof: REACT_ELEMENT_TYPE, + type: REACT_LEGACY_HIDDEN_TYPE, + key: null, + ref: null, + props: { + children: nextChildren, + // Check the host config to see if the children are offscreen/hidden. + mode: shouldDeprioritizeSubtree(type, nextProps) ? 'hidden' : 'visible', + }, + _owner: __DEV__ ? {} : null, + }; + nextChildren = wrappedChildren; } reconcileChildren( @@ -1651,33 +1717,32 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) { } } -function mountSuspenseState( +const SUSPENDED_MARKER: SuspenseState = { + dehydrated: null, + retryTime: NoWork, +}; + +function mountSuspenseOffscreenState( renderExpirationTime: ExpirationTimeOpaque, -): SuspenseState { +): OffscreenState { return { - dehydrated: null, baseTime: renderExpirationTime, - retryTime: NoWork, }; } -function updateSuspenseState( - prevSuspenseState: SuspenseState, +function updateSuspenseOffscreenState( + prevOffscreenState: OffscreenState, renderExpirationTime: ExpirationTimeOpaque, -): SuspenseState { - const prevSuspendedTime = prevSuspenseState.baseTime; +): OffscreenState { + const prevBaseTime = prevOffscreenState.baseTime; return { - dehydrated: null, + // Choose whichever time is inclusive of the other one. This represents + // the union of all the levels that suspended. baseTime: - // Choose whichever time is inclusive of the other one. This represents - // the union of all the levels that suspended. - !isSameExpirationTime( - prevSuspendedTime, - (NoWork: ExpirationTimeOpaque), - ) && !isSameOrHigherPriority(prevSuspendedTime, renderExpirationTime) - ? prevSuspendedTime + !isSameExpirationTime(prevBaseTime, (NoWork: ExpirationTimeOpaque)) && + !isSameOrHigherPriority(prevBaseTime, renderExpirationTime) + ? prevBaseTime : renderExpirationTime, - retryTime: NoWork, }; } @@ -1692,19 +1757,7 @@ function shouldRemainOnFallback( // For example, SuspenseList coordinates when nested content appears. if (current !== null) { const suspenseState: SuspenseState = current.memoizedState; - if (suspenseState !== null) { - // Currently showing a fallback. If the current render includes - // the level that triggered the fallback, we must continue showing it, - // regardless of what the Suspense context says. - const baseTime = suspenseState.baseTime; - if ( - !isSameExpirationTime(baseTime, (NoWork: ExpirationTimeOpaque)) && - !isSameOrHigherPriority(baseTime, renderExpirationTime) - ) { - return true; - } - // Otherwise, fall through to check the Suspense context. - } else { + if (suspenseState === null) { // Currently showing content. Don't hide it, even if ForceSuspenseFallack // is true. More precise name might be "ForceRemainSuspenseFallback". // Note: This is a factoring smell. Can't remain on a fallback if there's @@ -1712,6 +1765,7 @@ function shouldRemainOnFallback( return false; } } + // Not currently showing content. Consult the Suspense context. return hasSuspenseContext( suspenseContext, @@ -1725,20 +1779,6 @@ function getRemainingWorkInPrimaryTree( renderExpirationTime, ) { const currentChildExpirationTime = current.childExpirationTime_opaque; - const currentSuspenseState: SuspenseState = current.memoizedState; - if (currentSuspenseState !== null) { - // This boundary already timed out. Check if this render includes the level - // that previously suspended. - const baseTime = currentSuspenseState.baseTime; - if ( - !isSameExpirationTime(baseTime, (NoWork: ExpirationTimeOpaque)) && - !isSameOrHigherPriority(baseTime, renderExpirationTime) - ) { - // There's pending work at a lower level that might now be unblocked. - return baseTime; - } - } - if ( !isSameOrHigherPriority(currentChildExpirationTime, renderExpirationTime) ) { @@ -1880,8 +1920,10 @@ function updateSuspenseComponent( renderExpirationTime, ); const primaryChildFragment: Fiber = (workInProgress.child: any); - primaryChildFragment.memoizedState = ({baseTime: NoWork}: OffscreenState); - workInProgress.memoizedState = mountSuspenseState(renderExpirationTime); + primaryChildFragment.memoizedState = mountSuspenseOffscreenState( + renderExpirationTime, + ); + workInProgress.memoizedState = SUSPENDED_MARKER; return fallbackFragment; } else { const nextPrimaryChildren = nextProps.children; @@ -1935,14 +1977,10 @@ function updateSuspenseComponent( renderExpirationTime, ); const primaryChildFragment: Fiber = (workInProgress.child: any); - primaryChildFragment.memoizedState = ({ - baseTime: NoWork, - }: OffscreenState); - workInProgress.memoizedState = updateSuspenseState( - current.memoizedState, + primaryChildFragment.memoizedState = mountSuspenseOffscreenState( renderExpirationTime, ); - + workInProgress.memoizedState = SUSPENDED_MARKER; return fallbackChildFragment; } } @@ -1959,18 +1997,21 @@ function updateSuspenseComponent( renderExpirationTime, ); const primaryChildFragment: Fiber = (workInProgress.child: any); - primaryChildFragment.memoizedState = ({ - baseTime: NoWork, - }: OffscreenState); + const prevOffscreenState: OffscreenState | null = (current.child: any) + .memoizedState; + primaryChildFragment.memoizedState = + prevOffscreenState === null + ? mountSuspenseOffscreenState(renderExpirationTime) + : updateSuspenseOffscreenState( + prevOffscreenState, + renderExpirationTime, + ); primaryChildFragment.childExpirationTime_opaque = getRemainingWorkInPrimaryTree( current, workInProgress, renderExpirationTime, ); - workInProgress.memoizedState = updateSuspenseState( - current.memoizedState, - renderExpirationTime, - ); + workInProgress.memoizedState = SUSPENDED_MARKER; return fallbackChildFragment; } else { const nextPrimaryChildren = nextProps.children; @@ -1997,9 +2038,15 @@ function updateSuspenseComponent( renderExpirationTime, ); const primaryChildFragment: Fiber = (workInProgress.child: any); - primaryChildFragment.memoizedState = ({ - baseTime: NoWork, - }: OffscreenState); + const prevOffscreenState: OffscreenState | null = (current.child: any) + .memoizedState; + primaryChildFragment.memoizedState = + prevOffscreenState === null + ? mountSuspenseOffscreenState(renderExpirationTime) + : updateSuspenseOffscreenState( + prevOffscreenState, + renderExpirationTime, + ); primaryChildFragment.childExpirationTime_opaque = getRemainingWorkInPrimaryTree( current, workInProgress, @@ -2007,7 +2054,7 @@ function updateSuspenseComponent( ); // Skip the primary children, and continue working on the // fallback children. - workInProgress.memoizedState = mountSuspenseState(renderExpirationTime); + workInProgress.memoizedState = SUSPENDED_MARKER; return fallbackChildFragment; } else { // Still haven't timed out. Continue rendering the children, like we @@ -3215,21 +3262,6 @@ function beginWork( break; case HostComponent: pushHostContext(workInProgress); - if ( - workInProgress.mode & ConcurrentMode && - !isSameExpirationTime( - renderExpirationTime, - (Never: ExpirationTimeOpaque), - ) && - shouldDeprioritizeSubtree(workInProgress.type, newProps) - ) { - if (enableSchedulerTracing) { - markSpawnedWork((Never: ExpirationTimeOpaque)); - } - // Schedule this fiber to re-render at offscreen priority. Then bailout. - workInProgress.expirationTime_opaque = workInProgress.childExpirationTime_opaque = Never; - return null; - } break; case ClassComponent: { const Component = workInProgress.type; @@ -3384,6 +3416,23 @@ function beginWork( return null; } } + case OffscreenComponent: + case LegacyHiddenComponent: { + // Need to check if the tree still needs to be deferred. This is + // almost identical to the logic used in the normal update path, + // so we'll just enter that. The only difference is we'll bail out + // at the next level instead of this one, because the child props + // have not changed. Which is fine. + // TODO: Probably should refactor `beginWork` to split the bailout + // path from the normal path. I'm tempted to do a labeled break here + // but I won't :) + workInProgress.expirationTime_opaque = NoWork; + return updateOffscreenComponent( + current, + workInProgress, + renderExpirationTime, + ); + } } return bailoutOnAlreadyFinishedWork( current, @@ -3536,13 +3585,6 @@ function beginWork( renderExpirationTime, ); } - case OffscreenComponent: { - return updateOffscreenComponent( - current, - workInProgress, - renderExpirationTime, - ); - } case SimpleMemoComponent: { return updateSimpleMemoComponent( current, @@ -3609,6 +3651,20 @@ function beginWork( } break; } + case OffscreenComponent: { + return updateOffscreenComponent( + current, + workInProgress, + renderExpirationTime, + ); + } + case LegacyHiddenComponent: { + return updateLegacyHiddenComponent( + current, + workInProgress, + renderExpirationTime, + ); + } } invariant( false, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 2e430f94b9a1..1defb7340bf3 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -57,6 +57,7 @@ import { ScopeComponent, Block, OffscreenComponent, + LegacyHiddenComponent, } from './ReactWorkTags'; import { invokeGuardedCallback, @@ -817,6 +818,7 @@ function commitLifeCycles( case FundamentalComponent: case ScopeComponent: case OffscreenComponent: + case LegacyHiddenComponent: return; } invariant( @@ -847,7 +849,8 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) { unhideTextInstance(instance, node.memoizedProps); } } else if ( - node.tag === OffscreenComponent && + (node.tag === OffscreenComponent || + node.tag === LegacyHiddenComponent) && (node.memoizedState: OffscreenState) !== null && node !== finishedWork ) { @@ -1592,7 +1595,8 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { } break; } - case OffscreenComponent: { + case OffscreenComponent: + case LegacyHiddenComponent: { return; } } @@ -1731,7 +1735,8 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { } break; } - case OffscreenComponent: { + case OffscreenComponent: + case LegacyHiddenComponent: { const newState: OffscreenState | null = finishedWork.memoizedState; const isHidden = newState !== null; hideOrUnhideAllChildren(finishedWork, isHidden); diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index 620279aced2e..e801c0e91c82 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -56,6 +56,7 @@ import { ScopeComponent, Block, OffscreenComponent, + LegacyHiddenComponent, } from './ReactWorkTags'; import {NoMode, BlockingMode} from './ReactTypeOfMode'; import { @@ -131,6 +132,7 @@ import { renderDidSuspend, renderDidSuspendDelayIfPossible, renderHasNotSuspendedYet, + popRenderExpirationTime, } from './ReactFiberWorkLoop.new'; import {createFundamentalStateInstance} from './ReactFiberFundamental.new'; import {Never, isSameOrHigherPriority} from './ReactFiberExpirationTime.new'; @@ -1311,7 +1313,9 @@ function completeWork( return null; } break; - case OffscreenComponent: { + case OffscreenComponent: + case LegacyHiddenComponent: { + popRenderExpirationTime(workInProgress); if (current !== null) { const nextState: OffscreenState | null = workInProgress.memoizedState; const prevState: OffscreenState | null = current.memoizedState; diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js index 88d7511cef19..f1ed2e73cccd 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js @@ -55,7 +55,7 @@ import { didNotFindHydratableSuspenseInstance, } from './ReactFiberHostConfig'; import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags'; -import {Never, NoWork} from './ReactFiberExpirationTime.new'; +import {Never} from './ReactFiberExpirationTime.new'; // The deepest Fiber on the stack involved in a hydration context. // This may have been an insertion or a hydration. @@ -231,7 +231,6 @@ function tryHydrate(fiber, nextInstance) { if (suspenseInstance !== null) { const suspenseState: SuspenseState = { dehydrated: suspenseInstance, - baseTime: NoWork, retryTime: Never, }; fiber.memoizedState = suspenseState; diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js index 26912af8f11a..a45f1710138f 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js @@ -29,10 +29,6 @@ export type SuspenseState = {| // here to indicate that it is dehydrated (flag) and for quick access // to check things like isSuspenseInstancePending. dehydrated: null | SuspenseInstance, - // Represents the work that was deprioritized when we committed the fallback. - // The work outside the boundary already committed at this level, so we cannot - // unhide the content without including it. - baseTime: ExpirationTimeOpaque, // Represents the earliest expiration time we should attempt to hydrate // a dehydrated boundary at. // Never is the default for dehydrated boundaries. diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js index d7d5585b5cc4..64bed3225461 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js @@ -20,6 +20,8 @@ import { ContextProvider, SuspenseComponent, SuspenseListComponent, + OffscreenComponent, + LegacyHiddenComponent, } from './ReactWorkTags'; import {DidCapture, NoEffect, ShouldCapture} from './ReactSideEffectTags'; import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags'; @@ -33,6 +35,7 @@ import { popTopLevelContextObject as popTopLevelLegacyContextObject, } from './ReactFiberContext.new'; import {popProvider} from './ReactFiberNewContext.new'; +import {popRenderExpirationTime} from './ReactFiberWorkLoop.new'; import invariant from 'shared/invariant'; @@ -105,6 +108,10 @@ function unwindWork( case ContextProvider: popProvider(workInProgress); return null; + case OffscreenComponent: + case LegacyHiddenComponent: + popRenderExpirationTime(workInProgress); + return null; default: return null; } @@ -141,6 +148,10 @@ function unwindInterruptedWork(interruptedWork: Fiber) { case ContextProvider: popProvider(interruptedWork); break; + case OffscreenComponent: + case LegacyHiddenComponent: + popRenderExpirationTime(interruptedWork); + break; default: break; } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index a6c71a1560cd..3ad0221b226b 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -15,6 +15,7 @@ import type {Interaction} from 'scheduler/src/Tracing'; import type {SuspenseConfig} from './ReactFiberSuspenseConfig'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; import type {Effect as HookEffect} from './ReactFiberHooks.new'; +import type {StackCursor} from './ReactFiberStack.new'; import { warnAboutDeprecatedLifecycles, @@ -168,6 +169,11 @@ import { getIsUpdatingOpaqueValueInRenderPhaseInDEV, } from './ReactFiberHooks.new'; import {createCapturedValue} from './ReactCapturedValue'; +import { + push as pushToStack, + pop as popFromStack, + createCursor, +} from './ReactFiberStack.new'; import { recordCommitTime, @@ -231,6 +237,12 @@ let workInProgressRoot: FiberRoot | null = null; let workInProgress: Fiber | null = null; // The expiration time we're rendering let renderExpirationTime: ExpirationTimeOpaque = NoWork; + +// Stack that allows components to channge renderExpirationTime for its subtree +const renderExpirationTimeCursor: StackCursor = createCursor( + NoWork, +); + // Whether to root completed, errored, suspended, etc. let workInProgressRootExitStatus: RootExitStatus = RootIncomplete; // A fatal error, if one is thrown @@ -1265,6 +1277,19 @@ export function flushControlled(fn: () => mixed): void { } } +export function pushRenderExpirationTime( + fiber: Fiber, + subtreeRenderTime: ExpirationTimeOpaque, +) { + pushToStack(renderExpirationTimeCursor, renderExpirationTime, fiber); + renderExpirationTime = subtreeRenderTime; +} + +export function popRenderExpirationTime(fiber: Fiber) { + renderExpirationTime = renderExpirationTimeCursor.current; + popFromStack(renderExpirationTimeCursor, fiber); +} + function prepareFreshStack(root, expirationTime) { root.finishedWork = null; root.finishedExpirationTime_opaque = NoWork; diff --git a/packages/react-reconciler/src/ReactWorkTags.js b/packages/react-reconciler/src/ReactWorkTags.js index 56389ed31fb4..44717fd372e5 100644 --- a/packages/react-reconciler/src/ReactWorkTags.js +++ b/packages/react-reconciler/src/ReactWorkTags.js @@ -31,7 +31,8 @@ export type WorkTag = | 20 | 21 | 22 - | 23; + | 23 + | 24; export const FunctionComponent = 0; export const ClassComponent = 1; @@ -57,3 +58,4 @@ export const FundamentalComponent = 20; export const ScopeComponent = 21; export const Block = 22; export const OffscreenComponent = 23; +export const LegacyHiddenComponent = 24; diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js index f6e7d6938285..bf1f1a696ea2 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js @@ -3180,18 +3180,49 @@ describe('ReactSuspenseWithNoopRenderer', () => { }); setFallbackText('Still loading...'); - expect(Scheduler).toFlushAndYield([ - // First try to render the high pri update. We won't try to re-render - // the suspended tree during this pass, because it still has unfinished - // updates at a lower priority. - 'Loading...', - - // Now try the suspended update again. It's still suspended. - 'Suspend! [C]', - - // Then complete the update to the fallback. - 'Still loading...', - ]); + expect(Scheduler).toFlushAndYield( + gate(flags => + flags.new + ? [ + // First try to render the high pri update. Still suspended. + 'Suspend! [C]', + 'Loading...', + + // In the expiration times model, once the high pri update + // suspends, we can't be sure if there's additional work at a + // lower priority that might unblock the tree. We do know that + // there's a lower priority update *somehwere* in the entire + // root, though (the update to the fallback). So we try + // rendering one more time, just in case. + // TODO: We shouldn't need to do this with lanes, because we + // always know exactly which lanes have pending work in + // each tree. + 'Suspend! [C]', + + // Then complete the update to the fallback. + 'Still loading...', + ] + : [ + // In the old reconciler, we don't attempt to unhdie the + // Suspense boundary at high priority. Instead, we bailout, + // then try again at the original priority that the component + // suspended. This is mostly an implementation compromise, + // though there are some advantages to this behavior, because + // attempt to unhide could slow down the rest of the update. + // + // Render that only includes the fallback, since we bailed + // out on the primary tree. + 'Loading...', + + // Now try the suspended update again at the original + // priority. It's still suspended. + 'Suspend! [C]', + + // Then complete the update to the fallback. + 'Still loading...', + ], + ), + ); expect(root).toMatchRenderedOutput( <>