diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index d13bd2c66b8df..04ec3e9633a70 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -77,6 +77,9 @@ import { prepareToHydrateHostTextInstance, popHydrationState, } from './ReactFiberHydrationContext'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; +import {ProfileMode} from 'react-reconciler/src/ReactTypeOfMode'; +import {NoWork, Never} from './ReactFiberExpirationTime'; function markUpdate(workInProgress: Fiber) { // Tag the fiber with an update effect. This turns a Placement into @@ -531,6 +534,88 @@ if (supportsMutation) { }; } +function resetChildExpirationTime( + workInProgress: Fiber, + renderTime: ExpirationTime, +) { + if (renderTime !== Never && workInProgress.childExpirationTime === Never) { + // The children of this component are hidden. Don't bubble their + // expiration times. + return; + } + + let newChildExpirationTime = NoWork; + + // Bubble up the earliest expiration time. + if (enableProfilerTimer && workInProgress.mode & ProfileMode) { + // We're in profiling mode. + // Let's use this same traversal to update the render durations. + let actualDuration = workInProgress.actualDuration; + let treeBaseDuration = workInProgress.selfBaseDuration; + + // When a fiber is cloned, its actualDuration is reset to 0. + // This value will only be updated if work is done on the fiber (i.e. it doesn't bailout). + // When work is done, it should bubble to the parent's actualDuration. + // If the fiber has not been cloned though, (meaning no work was done), + // Then this value will reflect the amount of time spent working on a previous render. + // In that case it should not bubble. + // We determine whether it was cloned by comparing the child pointer. + const shouldBubbleActualDurations = + workInProgress.alternate === null || + workInProgress.child !== workInProgress.alternate.child; + + let child = workInProgress.child; + while (child !== null) { + const childUpdateExpirationTime = child.expirationTime; + const childChildExpirationTime = child.childExpirationTime; + if ( + newChildExpirationTime === NoWork || + (childUpdateExpirationTime !== NoWork && + childUpdateExpirationTime < newChildExpirationTime) + ) { + newChildExpirationTime = childUpdateExpirationTime; + } + if ( + newChildExpirationTime === NoWork || + (childChildExpirationTime !== NoWork && + childChildExpirationTime < newChildExpirationTime) + ) { + newChildExpirationTime = childChildExpirationTime; + } + if (shouldBubbleActualDurations) { + actualDuration += child.actualDuration; + } + treeBaseDuration += child.treeBaseDuration; + child = child.sibling; + } + workInProgress.actualDuration = actualDuration; + workInProgress.treeBaseDuration = treeBaseDuration; + } else { + let child = workInProgress.child; + while (child !== null) { + const childUpdateExpirationTime = child.expirationTime; + const childChildExpirationTime = child.childExpirationTime; + if ( + newChildExpirationTime === NoWork || + (childUpdateExpirationTime !== NoWork && + childUpdateExpirationTime < newChildExpirationTime) + ) { + newChildExpirationTime = childUpdateExpirationTime; + } + if ( + newChildExpirationTime === NoWork || + (childChildExpirationTime !== NoWork && + childChildExpirationTime < newChildExpirationTime) + ) { + newChildExpirationTime = childChildExpirationTime; + } + child = child.sibling; + } + } + + workInProgress.childExpirationTime = newChildExpirationTime; +} + function completeWork( current: Fiber | null, workInProgress: Fiber, @@ -540,15 +625,16 @@ function completeWork( switch (workInProgress.tag) { case IndeterminateComponent: - break; case FunctionComponent: case FunctionComponentLazy: + resetChildExpirationTime(workInProgress, renderExpirationTime); break; case ClassComponent: { const Component = workInProgress.type; if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } + resetChildExpirationTime(workInProgress, renderExpirationTime); break; } case ClassComponentLazy: { @@ -556,6 +642,7 @@ function completeWork( if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } + resetChildExpirationTime(workInProgress, renderExpirationTime); break; } case HostRoot: { @@ -575,6 +662,7 @@ function completeWork( workInProgress.effectTag &= ~Placement; } updateHostContainer(workInProgress); + resetChildExpirationTime(workInProgress, renderExpirationTime); break; } case HostComponent: { @@ -657,6 +745,7 @@ function completeWork( markRef(workInProgress); } } + resetChildExpirationTime(workInProgress, renderExpirationTime); break; } case HostText: { @@ -691,10 +780,12 @@ function completeWork( ); } } + resetChildExpirationTime(workInProgress, renderExpirationTime); break; } case ForwardRef: case ForwardRefLazy: + resetChildExpirationTime(workInProgress, renderExpirationTime); break; case SuspenseComponent: { const nextState = workInProgress.memoizedState; @@ -706,26 +797,33 @@ function completeWork( // and the timed-out state, schedule an effect. workInProgress.effectTag |= Update; } + resetChildExpirationTime(workInProgress, renderExpirationTime); + if (nextDidTimeout) { + const fallbackFragment: Fiber = (workInProgress.child: any).sibling; + workInProgress.childExpirationTime = + fallbackFragment.childExpirationTime; + } break; } case Fragment: - break; case Mode: - break; case Profiler: + resetChildExpirationTime(workInProgress, renderExpirationTime); break; case HostPortal: popHostContainer(workInProgress); updateHostContainer(workInProgress); + resetChildExpirationTime(workInProgress, renderExpirationTime); break; case ContextProvider: // Pop provider fiber popProvider(workInProgress); + resetChildExpirationTime(workInProgress, renderExpirationTime); break; case ContextConsumer: - break; case PureComponent: case PureComponentLazy: + resetChildExpirationTime(workInProgress, renderExpirationTime); break; default: invariant( diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 480909ac6246f..73bb663f31972 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -805,88 +805,6 @@ function commitRoot(root: FiberRoot, finishedWork: Fiber): void { } } -function resetChildExpirationTime( - workInProgress: Fiber, - renderTime: ExpirationTime, -) { - if (renderTime !== Never && workInProgress.childExpirationTime === Never) { - // The children of this component are hidden. Don't bubble their - // expiration times. - return; - } - - let newChildExpirationTime = NoWork; - - // Bubble up the earliest expiration time. - if (enableProfilerTimer && workInProgress.mode & ProfileMode) { - // We're in profiling mode. - // Let's use this same traversal to update the render durations. - let actualDuration = workInProgress.actualDuration; - let treeBaseDuration = workInProgress.selfBaseDuration; - - // When a fiber is cloned, its actualDuration is reset to 0. - // This value will only be updated if work is done on the fiber (i.e. it doesn't bailout). - // When work is done, it should bubble to the parent's actualDuration. - // If the fiber has not been cloned though, (meaning no work was done), - // Then this value will reflect the amount of time spent working on a previous render. - // In that case it should not bubble. - // We determine whether it was cloned by comparing the child pointer. - const shouldBubbleActualDurations = - workInProgress.alternate === null || - workInProgress.child !== workInProgress.alternate.child; - - let child = workInProgress.child; - while (child !== null) { - const childUpdateExpirationTime = child.expirationTime; - const childChildExpirationTime = child.childExpirationTime; - if ( - newChildExpirationTime === NoWork || - (childUpdateExpirationTime !== NoWork && - childUpdateExpirationTime < newChildExpirationTime) - ) { - newChildExpirationTime = childUpdateExpirationTime; - } - if ( - newChildExpirationTime === NoWork || - (childChildExpirationTime !== NoWork && - childChildExpirationTime < newChildExpirationTime) - ) { - newChildExpirationTime = childChildExpirationTime; - } - if (shouldBubbleActualDurations) { - actualDuration += child.actualDuration; - } - treeBaseDuration += child.treeBaseDuration; - child = child.sibling; - } - workInProgress.actualDuration = actualDuration; - workInProgress.treeBaseDuration = treeBaseDuration; - } else { - let child = workInProgress.child; - while (child !== null) { - const childUpdateExpirationTime = child.expirationTime; - const childChildExpirationTime = child.childExpirationTime; - if ( - newChildExpirationTime === NoWork || - (childUpdateExpirationTime !== NoWork && - childUpdateExpirationTime < newChildExpirationTime) - ) { - newChildExpirationTime = childUpdateExpirationTime; - } - if ( - newChildExpirationTime === NoWork || - (childChildExpirationTime !== NoWork && - childChildExpirationTime < newChildExpirationTime) - ) { - newChildExpirationTime = childChildExpirationTime; - } - child = child.sibling; - } - } - - workInProgress.childExpirationTime = newChildExpirationTime; -} - function completeUnitOfWork(workInProgress: Fiber): Fiber | null { // Attempt to complete the current unit of work, then move to the // next sibling. If there are no more siblings, return to the @@ -929,7 +847,6 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null { ); } stopWorkTimer(workInProgress); - resetChildExpirationTime(workInProgress, nextRenderExpirationTime); if (__DEV__) { ReactCurrentFiber.resetCurrentFiber(); } diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index 7386db830dcad..47dbb30d97f64 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -435,5 +435,53 @@ describe('ReactSuspense', () => { ]); expect(root).toMatchRenderedOutput('AB:2C'); }); + + it('bails out on timed-out primary children even if they receive an update', () => { + let instance; + class Stateful extends React.Component { + state = {step: 1}; + render() { + instance = this; + return ; + } + } + + function App(props) { + return ( + }> + + + + ); + } + + const root = ReactTestRenderer.create(); + + expect(ReactTestRenderer).toHaveYielded([ + 'Stateful', + 'Suspend! [A]', + 'Loading...', + ]); + + jest.advanceTimersByTime(1000); + expect(ReactTestRenderer).toHaveYielded(['Promise resolved [A]', 'A']); + expect(root).toMatchRenderedOutput('StatefulA'); + + root.update(); + expect(ReactTestRenderer).toHaveYielded([ + 'Stateful', + 'Suspend! [B]', + 'Loading...', + ]); + + instance.setState({step: 2}); + + jest.advanceTimersByTime(1000); + expect(ReactTestRenderer).toHaveYielded([ + 'Promise resolved [B]', + 'Stateful', + 'B', + ]); + }); }); });