From 8c452d8b96825112c1d2fd0c8e33cf6683554735 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sun, 15 Oct 2017 22:09:17 -0700 Subject: [PATCH 1/2] API for prerendering a top-level update and deferring the commit Adds the ability to start rendering work without flushing the changes to the screen, by blocking the commit phase. This can be used to coordinate React's commit phase with other async work. - root.prerender schedules an update and returns a work object. - work.commit unblocks the commit phase and flushes the remaining work synchronously. (Not yet implemented is work.then, which schedules a callback that fires once the work has completed.) --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 25 +++ .../dom/shared/__tests__/ReactDOMRoot-test.js | 10 + .../shared/fiber/ReactFiberCompleteWork.js | 7 + .../shared/fiber/ReactFiberReconciler.js | 73 ++++++- src/renderers/shared/fiber/ReactFiberRoot.js | 8 + .../shared/fiber/ReactFiberScheduler.js | 181 ++++++++++-------- 6 files changed, 222 insertions(+), 82 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index d319b9d2d87..c86a616db61 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -11,6 +11,7 @@ 'use strict'; import type {ReactNodeList} from 'ReactTypes'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; require('checkReact'); var DOMNamespaces = require('DOMNamespaces'); @@ -757,8 +758,27 @@ function createPortal( return ReactPortal.createPortal(children, container, null, key); } +type WorkNode = { + commit(): void, + + _reactRootContainer: *, + _expirationTime: ExpirationTime, +}; + +function Work(root: *, expirationTime: ExpirationTime) { + this._reactRootContainer = root; + this._expirationTime = expirationTime; +} +Work.prototype.commit = function() { + const root = this._reactRootContainer; + const expirationTime = this._expirationTime; + DOMRenderer.unblockRoot(root, expirationTime); + DOMRenderer.flushRoot(root, expirationTime); +}; + type ReactRootNode = { render(children: ReactNodeList, callback: ?() => mixed): void, + prerender(children: ReactNodeList): WorkNode, unmount(callback: ?() => mixed): void, _reactRootContainer: *, @@ -779,6 +799,11 @@ ReactRoot.prototype.render = function( const root = this._reactRootContainer; DOMRenderer.updateContainer(children, root, null, callback); }; +ReactRoot.prototype.prerender = function(children: ReactNodeList): WorkNode { + const root = this._reactRootContainer; + const expirationTime = DOMRenderer.updateRoot(children, root, null, null); + return new Work(root, expirationTime); +}; ReactRoot.prototype.unmount = function(callback) { const root = this._reactRootContainer; DOMRenderer.updateContainer(null, root, null, callback); diff --git a/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js b/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js index c4a327b116e..d38d78ce6ac 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js @@ -68,4 +68,14 @@ describe('ReactDOMRoot', () => { root.render(
dc
); expect(container.textContent).toEqual('abdc'); }); + + it('can defer commit using prerender', () => { + const root = ReactDOM.createRoot(container); + const work = root.prerender(
Hi
); + // Hasn't updated yet + expect(container.textContent).toEqual(''); + // Flush work + work.commit(); + expect(container.textContent).toEqual('Hi'); + }); }); diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index edb6048e913..8299b959d71 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -48,6 +48,7 @@ module.exports = function( config: HostConfig, hostContext: HostContext, hydrationContext: HydrationContext, + blockCurrentlyRenderingWork: () => void, ) { const { createInstance, @@ -219,6 +220,12 @@ module.exports = function( // TODO: Delete this when we delete isMounted and findDOMNode. workInProgress.effectTag &= ~Placement; } + + const memoizedState = workInProgress.memoizedState; + if (memoizedState !== null && memoizedState.isBlocked) { + // Root is blocked by a top-level update. + blockCurrentlyRenderingWork(); + } return null; } case HostComponent: { diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 7f6a7c3017c..0629d7bc9dd 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -13,6 +13,7 @@ import type {Fiber} from 'ReactFiber'; import type {FiberRoot} from 'ReactFiberRoot'; import type {ReactNodeList} from 'ReactTypes'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; var ReactFeatureFlags = require('ReactFeatureFlags'); var { @@ -226,6 +227,14 @@ export type Reconciler = { parentComponent: ?React$Component, callback: ?Function, ): void, + updateRoot( + element: ReactNodeList, + container: OpaqueRoot, + parentComponent: ?React$Component, + callback: ?Function, + ): ExpirationTime, + unblockRoot(root: OpaqueRoot, expirationTime: ExpirationTime): void, + flushRoot(root: OpaqueRoot, expirationTime: ExpirationTime): void, batchedUpdates(fn: () => A): A, unbatchedUpdates(fn: () => A): A, flushSync(fn: () => A): A, @@ -266,6 +275,7 @@ module.exports = function( computeAsyncExpiration, computeExpirationForFiber, scheduleWork, + expireWork, batchedUpdates, unbatchedUpdates, flushSync, @@ -275,8 +285,9 @@ module.exports = function( function scheduleTopLevelUpdate( current: Fiber, element: ReactNodeList, + isBlocked: boolean, callback: ?Function, - ) { + ): ExpirationTime { if (__DEV__) { if ( ReactDebugCurrentFiber.phase === 'render' && @@ -323,15 +334,15 @@ module.exports = function( const update = { expirationTime, - partialState: {element}, + partialState: {element, isBlocked}, callback, isReplace: false, isForced: false, - nextCallback: null, next: null, }; insertUpdateIntoFiber(current, update); scheduleWork(current, expirationTime); + return expirationTime; } return { @@ -367,7 +378,61 @@ module.exports = function( container.pendingContext = context; } - scheduleTopLevelUpdate(current, element, callback); + scheduleTopLevelUpdate(current, element, false, callback); + }, + + // Like updateContainer, but blocks the root from committing. Returns an + // expiration time. + // TODO: Can this be unified with updateContainer? Or is it incompatible + // with the existing semantics of ReactDOM.render? + updateRoot( + element: ReactNodeList, + container: OpaqueRoot, + parentComponent: ?React$Component, + callback: ?Function, + ): ExpirationTime { + // TODO: If this is a nested container, this won't be the root. + const current = container.current; + + if (__DEV__) { + if (ReactFiberInstrumentation.debugTool) { + if (current.alternate === null) { + ReactFiberInstrumentation.debugTool.onMountContainer(container); + } else if (element === null) { + ReactFiberInstrumentation.debugTool.onUnmountContainer(container); + } else { + ReactFiberInstrumentation.debugTool.onUpdateContainer(container); + } + } + } + + const context = getContextForSubtree(parentComponent); + if (container.context === null) { + container.context = context; + } else { + container.pendingContext = context; + } + + return scheduleTopLevelUpdate(current, element, true, callback); + }, + + unblockRoot(root: OpaqueRoot, expirationTime: ExpirationTime) { + const current = root.current; + const partialState = {isBlocked: false}; + const update = { + expirationTime, + partialState, + callback: null, + isReplace: false, + isForced: false, + next: null, + }; + insertUpdateIntoFiber(current, update); + scheduleWork(current, expirationTime); + }, + + flushRoot(root: OpaqueRoot, expirationTime: ExpirationTime) { + expireWork(root, expirationTime); }, batchedUpdates, diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js index 419e842e08b..0c4ac7334a4 100644 --- a/src/renderers/shared/fiber/ReactFiberRoot.js +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -11,8 +11,10 @@ 'use strict'; import type {Fiber} from 'ReactFiber'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; const {createHostRootFiber} = require('ReactFiber'); +const {NoWork} = require('ReactFiberExpirationTime'); export type FiberRoot = { // Any additional information from the host associated with this root. @@ -21,6 +23,10 @@ export type FiberRoot = { current: Fiber, // Determines if this root has already been added to the schedule for work. isScheduled: boolean, + // Determines if this root was blocked from committing. + isBlocked: boolean, + // The time at which this root completed. + forceExpire: ExpirationTime, // The work schedule is a linked list. nextScheduledRoot: FiberRoot | null, // Top context object, used by renderSubtreeIntoContainer @@ -41,6 +47,8 @@ exports.createFiberRoot = function( current: uninitializedFiber, containerInfo: containerInfo, isScheduled: false, + isBlocked: false, + forceExpire: NoWork, nextScheduledRoot: null, context: null, pendingContext: null, diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 50f16402d06..28a6fa877e8 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -172,6 +172,7 @@ module.exports = function( config, hostContext, hydrationContext, + blockCurrentlyRenderingRoot, ); const { commitResetTextContent, @@ -216,6 +217,10 @@ module.exports = function( let nextUnitOfWork: Fiber | null = null; // The time at which we're currently rendering work. let nextRenderExpirationTime: ExpirationTime = NoWork; + // The root that we're currently working on. + let nextRenderedTree: FiberRoot | null = null; + // Whether the root we're currently working on is blocked from committing. + let nextCommitIsBlocked: boolean = false; // The next fiber with an effect that we're currently committing. let nextEffect: Fiber | null = null; @@ -248,7 +253,6 @@ module.exports = function( // Use these to prevent an infinite loop of nested updates const NESTED_UPDATE_LIMIT = 1000; let nestedUpdateCount: number = 0; - let nextRenderedTree: FiberRoot | null = null; function resetContextStack() { // Reset the stack @@ -286,13 +290,17 @@ module.exports = function( let earliestExpirationRoot = null; let earliestExpirationTime = NoWork; while (root !== null) { - if ( - root.current.expirationTime !== NoWork && - (earliestExpirationTime === NoWork || - earliestExpirationTime > root.current.expirationTime) - ) { - earliestExpirationTime = root.current.expirationTime; - earliestExpirationRoot = root; + if (root.isBlocked) { + // TODO: Process completion callbacks + } else { + if ( + root.current.expirationTime !== NoWork && + (earliestExpirationTime === NoWork || + earliestExpirationTime > root.current.expirationTime) + ) { + earliestExpirationTime = root.current.expirationTime; + earliestExpirationRoot = root; + } } // We didn't find anything to do in this root, so let's try the next one. root = root.nextScheduledRoot; @@ -315,15 +323,21 @@ module.exports = function( nestedUpdateCount = 0; nextRenderedTree = earliestExpirationRoot; } + nextCommitIsBlocked = false; return; } nextRenderExpirationTime = NoWork; nextUnitOfWork = null; nextRenderedTree = null; + nextCommitIsBlocked = false; return; } + function blockCurrentlyRenderingRoot() { + nextCommitIsBlocked = true; + } + function commitAllHostEffects() { while (nextEffect !== null) { if (__DEV__) { @@ -702,10 +716,13 @@ module.exports = function( workInProgress = returnFiber; continue; } else { - // We've reached the root. Mark the root as pending commit. Depending - // on how much time we have left, we'll either commit it now or in - // the next frame. - pendingCommit = workInProgress; + // We've reached the root. + const root: FiberRoot = workInProgress.stateNode; + if (nextCommitIsBlocked) { + root.isBlocked = true; + } else { + pendingCommit = workInProgress; + } return null; } } @@ -808,13 +825,10 @@ module.exports = function( nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } if (nextUnitOfWork === null) { - invariant( - pendingCommit !== null, - 'Should have a pending commit. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - // We just completed a root. Commit it now. - commitAllWork(pendingCommit); + if (pendingCommit !== null) { + // We just completed a root. Commit it now. + commitAllWork(pendingCommit); + } if ( capturedErrors === null || capturedErrors.size === 0 || @@ -856,15 +870,12 @@ module.exports = function( while (nextUnitOfWork !== null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); if (nextUnitOfWork === null) { - invariant( - pendingCommit !== null, - 'Should have a pending commit. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - // We just completed a root. Commit it now. - commitAllWork(pendingCommit); - // Clear any errors that were scheduled during the commit phase. - handleCommitPhaseErrors(); + if (pendingCommit !== null) { + // We just completed a root. Commit it now. + commitAllWork(pendingCommit); + // Clear any errors that were scheduled during the commit phase. + handleCommitPhaseErrors(); + } // The render time may have changed. Check again. if ( nextRenderExpirationTime === NoWork || @@ -886,28 +897,26 @@ module.exports = function( // omit either of the checks in the following condition, but we need // both to satisfy Flow. if (nextUnitOfWork === null) { - invariant( - pendingCommit !== null, - 'Should have a pending commit. This error is likely caused by ' + - 'a bug in React. Please file an issue.', - ); - // We just completed a root. If we have time, commit it now. - // Otherwise, we'll commit it in the next frame. - if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { - commitAllWork(pendingCommit); - // Clear any errors that were scheduled during the commit phase. - handleCommitPhaseErrors(); - // The render time may have changed. Check again. - if ( - nextRenderExpirationTime === NoWork || - nextRenderExpirationTime > minExpirationTime || - nextRenderExpirationTime <= mostRecentCurrentTime - ) { - // We've completed all the async work. - break; + if (pendingCommit !== null) { + // We just completed a root. If we have time, commit it now. + // Otherwise, we'll commit it in the next frame. + if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { + commitAllWork(pendingCommit); + // Clear any errors that were scheduled during the + // commit phase. + handleCommitPhaseErrors(); + } else { + deadlineHasExpired = true; } - } else { - deadlineHasExpired = true; + } + // The render time may have changed. Check again. + if ( + nextRenderExpirationTime === NoWork || + nextRenderExpirationTime > minExpirationTime || + nextRenderExpirationTime <= mostRecentCurrentTime + ) { + // We've completed all the async work. + break; } } } else { @@ -1354,25 +1363,6 @@ module.exports = function( } } - function scheduleRoot(root: FiberRoot, expirationTime: ExpirationTime) { - if (expirationTime === NoWork) { - return; - } - - if (!root.isScheduled) { - root.isScheduled = true; - if (lastScheduledRoot) { - // Schedule ourselves to the end. - lastScheduledRoot.nextScheduledRoot = root; - lastScheduledRoot = root; - } else { - // We're the only work scheduled. - nextScheduledRoot = root; - lastScheduledRoot = root; - } - } - } - function computeAsyncExpiration() { // Given the current clock time, returns an expiration time. We use rounding // to batch like updates together. @@ -1461,17 +1451,12 @@ module.exports = function( let node = fiber; let shouldContinue = true; while (node !== null && shouldContinue) { - // Walk the parent path to the root and update each node's expiration - // time. Once we reach a node whose expiration matches (and whose - // alternate's expiration matches) we can exit safely knowing that the - // rest of the path is correct. - shouldContinue = false; + // Walk the parent path to the root and update each node's + // expiration time. if ( node.expirationTime === NoWork || node.expirationTime > expirationTime ) { - // Expiration time did not match. Update and keep going. - shouldContinue = true; node.expirationTime = expirationTime; } if (node.alternate !== null) { @@ -1479,15 +1464,29 @@ module.exports = function( node.alternate.expirationTime === NoWork || node.alternate.expirationTime > expirationTime ) { - // Expiration time did not match. Update and keep going. - shouldContinue = true; node.alternate.expirationTime = expirationTime; } } if (node.return === null) { if (node.tag === HostRoot) { const root: FiberRoot = (node.stateNode: any); - scheduleRoot(root, expirationTime); + + // Set this to false in case the root has been unblocked. + root.isBlocked = false; + + if (!root.isScheduled) { + root.isScheduled = true; + if (lastScheduledRoot) { + // Schedule ourselves to the end. + lastScheduledRoot.nextScheduledRoot = root; + lastScheduledRoot = root; + } else { + // We're the only work scheduled. + nextScheduledRoot = root; + lastScheduledRoot = root; + } + } + if (!isPerformingWork) { switch (expirationTime) { case Sync: @@ -1535,12 +1534,37 @@ module.exports = function( } function recalculateCurrentTime(): ExpirationTime { + if (nextRenderedTree !== null) { + // Check if the current root is being force expired. + const forceExpire = nextRenderedTree.forceExpire; + if (forceExpire !== NoWork) { + // Override the current time with the `forceExpire` time. This has the + // effect of expiring all work up to and including that time. + mostRecentCurrentTime = forceExpire; + return forceExpire; + } + } // Subtract initial time so it fits inside 32bits const ms = now() - startTime; mostRecentCurrentTime = msToExpirationTime(ms); return mostRecentCurrentTime; } + function expireWork(root: FiberRoot, expirationTime: ExpirationTime): void { + invariant( + !isPerformingWork, + 'Cannot commit while already performing work.', + ); + root.forceExpire = expirationTime; + root.isBlocked = false; + try { + performWork(expirationTime, null); + } finally { + root.forceExpire = NoWork; + recalculateCurrentTime(); + } + } + function batchedUpdates(fn: (a: A) => R, a: A): R { const previousIsBatchingUpdates = isBatchingUpdates; isBatchingUpdates = true; @@ -1604,6 +1628,7 @@ module.exports = function( computeAsyncExpiration: computeAsyncExpiration, computeExpirationForFiber: computeExpirationForFiber, scheduleWork: scheduleWork, + expireWork: expireWork, batchedUpdates: batchedUpdates, unbatchedUpdates: unbatchedUpdates, flushSync: flushSync, From decebe3e96fb9fe529deae5964dd8da17eb95263 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 16 Oct 2017 00:44:45 -0700 Subject: [PATCH 2/2] Basic version of resuming for top-level prerender Implements a limited version of resuming that only applies to the root, to account for the pathological case where a prerendered root must be completely restarted before it can commit. This won't be necessary once we implement resuming for real. --- .../dom/shared/__tests__/ReactDOMRoot-test.js | 20 ++++++++++++++ .../shared/fiber/ReactFiberBeginWork.js | 13 ++++++++-- src/renderers/shared/fiber/ReactFiberRoot.js | 3 +++ .../shared/fiber/ReactFiberScheduler.js | 26 ++++++++++++++++--- 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js b/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js index d38d78ce6ac..498be74e378 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js @@ -78,4 +78,24 @@ describe('ReactDOMRoot', () => { work.commit(); expect(container.textContent).toEqual('Hi'); }); + + it("does not restart a blocked root that wasn't updated", () => { + let ops = []; + function Foo(props) { + ops.push('Foo'); + return props.children; + } + const root = ReactDOM.createRoot(container); + const work = root.prerender(Hi); + expect(ops).toEqual(['Foo']); + // Hasn't updated yet + expect(container.textContent).toEqual(''); + + ops = []; + + // Flush work. Shouldn't re-render Foo. + work.commit(); + expect(ops).toEqual([]); + expect(container.textContent).toEqual('Hi'); + }); }); diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 508a41a8622..347116ceafe 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -319,6 +319,7 @@ module.exports = function( } function updateHostRoot(current, workInProgress, renderExpirationTime) { + const root: FiberRoot = workInProgress.stateNode; pushHostRootContext(workInProgress); const updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { @@ -331,6 +332,16 @@ module.exports = function( null, renderExpirationTime, ); + memoizeState(workInProgress, state); + if (root.completedAt === renderExpirationTime) { + // The root is already complete. Bail out and commit. + // TODO: This is a limited version of resuming that only applies to + // the root, to account for the pathological case where a completed + // root must be completely restarted before it can commit. Once we + // implement resuming for real, this special branch shouldn't + // be neccessary. + return null; + } if (prevState === state) { // If the state is the same as before, that's a bailout because we had // no work that expires at this time. @@ -338,7 +349,6 @@ module.exports = function( return bailoutOnAlreadyFinishedWork(current, workInProgress); } const element = state.element; - const root: FiberRoot = workInProgress.stateNode; if ( (current === null || current.child === null) && root.hydrate && @@ -370,7 +380,6 @@ module.exports = function( resetHydrationState(); reconcileChildren(current, workInProgress, element); } - memoizeState(workInProgress, state); return workInProgress.child; } resetHydrationState(); diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js index 0c4ac7334a4..3045d2eacb1 100644 --- a/src/renderers/shared/fiber/ReactFiberRoot.js +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -26,6 +26,8 @@ export type FiberRoot = { // Determines if this root was blocked from committing. isBlocked: boolean, // The time at which this root completed. + // TODO: Remove once we add back resuming. + completedAt: ExpirationTime, forceExpire: ExpirationTime, // The work schedule is a linked list. nextScheduledRoot: FiberRoot | null, @@ -48,6 +50,7 @@ exports.createFiberRoot = function( containerInfo: containerInfo, isScheduled: false, isBlocked: false, + completedAt: NoWork, forceExpire: NoWork, nextScheduledRoot: null, context: null, diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 28a6fa877e8..6d0c8b9cb38 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -314,10 +314,26 @@ module.exports = function( // unfortunately this is it. resetContextStack(); - nextUnitOfWork = createWorkInProgress( - earliestExpirationRoot.current, - earliestExpirationTime, - ); + if (earliestExpirationRoot.completedAt === nextRenderExpirationTime) { + // If the root is already complete, reuse the existing work-in-progress. + // TODO: This is a limited version of resuming that only applies to + // the root, to account for the pathological case where a completed + // root must be completely restarted before it can commit. Once we + // implement resuming for real, this special branch shouldn't + // be neccessary. + nextUnitOfWork = earliestExpirationRoot.current.alternate; + invariant( + nextUnitOfWork !== null, + 'Expected a completed root to have a work-in-progress. This error ' + + 'is likely caused by a bug in React. Please file an issue.', + ); + } else { + nextUnitOfWork = createWorkInProgress( + earliestExpirationRoot.current, + earliestExpirationTime, + ); + } + if (earliestExpirationRoot !== nextRenderedTree) { // We've switched trees. Reset the nested update counter. nestedUpdateCount = 0; @@ -462,6 +478,7 @@ module.exports = function( 'related to the return field. This error is likely caused by a bug ' + 'in React. Please file an issue.', ); + root.completedAt = NoWork; if (nextRenderExpirationTime <= mostRecentCurrentTime) { // Keep track of the number of iterations to prevent an infinite @@ -718,6 +735,7 @@ module.exports = function( } else { // We've reached the root. const root: FiberRoot = workInProgress.stateNode; + root.completedAt = nextRenderExpirationTime; if (nextCommitIsBlocked) { root.isBlocked = true; } else {