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..498be74e378 100644
--- a/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js
+++ b/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js
@@ -68,4 +68,34 @@ 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');
+ });
+
+ 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/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..3045d2eacb1 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,12 @@ 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.
+ // TODO: Remove once we add back resuming.
+ completedAt: ExpirationTime,
+ forceExpire: ExpirationTime,
// The work schedule is a linked list.
nextScheduledRoot: FiberRoot | null,
// Top context object, used by renderSubtreeIntoContainer
@@ -41,6 +49,9 @@ exports.createFiberRoot = function(
current: uninitializedFiber,
containerInfo: containerInfo,
isScheduled: false,
+ isBlocked: false,
+ completedAt: NoWork,
+ 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..6d0c8b9cb38 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;
@@ -306,24 +314,46 @@ 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;
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__) {
@@ -448,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
@@ -702,10 +733,14 @@ 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;
+ root.completedAt = nextRenderExpirationTime;
+ if (nextCommitIsBlocked) {
+ root.isBlocked = true;
+ } else {
+ pendingCommit = workInProgress;
+ }
return null;
}
}
@@ -808,13 +843,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 +888,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 +915,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 +1381,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 +1469,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 +1482,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 +1552,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 +1646,7 @@ module.exports = function(
computeAsyncExpiration: computeAsyncExpiration,
computeExpirationForFiber: computeExpirationForFiber,
scheduleWork: scheduleWork,
+ expireWork: expireWork,
batchedUpdates: batchedUpdates,
unbatchedUpdates: unbatchedUpdates,
flushSync: flushSync,