diff --git a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
index c4a327b116e..09a743020c0 100644
--- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
@@ -13,6 +13,8 @@ var React = require('react');
var ReactDOM = require('react-dom');
var ReactDOMServer = require('react-dom/server');
+const AsyncComponent = React.unstable_AsyncComponent;
+
describe('ReactDOMRoot', () => {
let container;
@@ -68,4 +70,114 @@ 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');
+ });
+
+ it('can wait for prerender to finish', () => {
+ const Async = React.unstable_AsyncComponent;
+ const root = ReactDOM.createRoot(container);
+ const work = root.prerender(Foo);
+
+ // Hasn't updated yet
+ expect(container.textContent).toEqual('');
+
+ let ops = [];
+ work.then(() => {
+ // Still hasn't updated
+ ops.push(container.textContent);
+ // Should synchronously commit
+ work.commit();
+ ops.push(container.textContent);
+ });
+ // Flush async work
+ jest.runAllTimers();
+ expect(ops).toEqual(['', 'Foo']);
+ });
+
+ it('resolves `then` callback synchronously if update is sync', () => {
+ const root = ReactDOM.createRoot(container);
+ const work = root.prerender(Hi
);
+
+ let ops = [];
+ work.then(() => {
+ work.commit();
+ ops.push(container.textContent);
+ expect(container.textContent).toEqual('Hi');
+ });
+ // `then` should have synchronously resolved
+ expect(ops).toEqual(['Hi']);
+ });
+
+ it('resolves `then` callback if tree already completed', () => {
+ const root = ReactDOM.createRoot(container);
+ const work = root.prerender(Hi
);
+
+ let ops = [];
+ work.then(() => {
+ work.commit();
+ ops.push(container.textContent);
+ expect(container.textContent).toEqual('Hi');
+ });
+
+ work.then(() => {
+ ops.push('Second callback');
+ });
+
+ // `then` should have synchronously resolved
+ expect(ops).toEqual(['Hi', 'Second callback']);
+ });
+
+ it('commits an earlier time without unblocking a later time', () => {
+ const root = ReactDOM.createRoot(container);
+ // Sync update
+ const work1 = root.prerender(a
);
+ // Async update
+ const work2 = root.prerender(b);
+ // Flush only the sync update
+ work1.commit();
+ jest.runAllTimers();
+ expect(container.textContent).toBe('a');
+ // Now flush the async update
+ work2.commit();
+ expect(container.textContent).toBe('b');
+ });
+
+ it('render returns a work object, too', () => {
+ const root = ReactDOM.createRoot(container);
+ const work = root.render(Hello
);
+ let ops = [];
+ work.then(() => {
+ // Work already committed.
+ ops.push(container.textContent);
+ });
+ expect(container.textContent).toEqual('Hello');
+ });
});
diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js
index 0de484d48cf..107c5c38440 100644
--- a/packages/react-dom/src/client/ReactDOM.js
+++ b/packages/react-dom/src/client/ReactDOM.js
@@ -10,6 +10,17 @@
'use strict';
import type {ReactNodeList} from 'shared/ReactTypes';
+// TODO: This type is shared between the reconciler and ReactDOM, but will
+// eventually be lifted out to the renderer.
+import type {
+ FiberRoot,
+ WorkNode as FiberRootWorkNode,
+} from 'react-reconciler/src/ReactFiberRoot';
+// TODO: ExpirationTime (or whatever we end up calling it) should be a public
+// type that renderers can consume.
+import type {
+ ExpirationTime,
+} from 'react-reconciler/src/ReactFiberExpirationTime';
require('../shared/checkReact');
@@ -759,9 +770,90 @@ function createPortal(
return ReactPortal.createPortal(children, container, null, key);
}
+type WorkNode = FiberRootWorkNode & {
+ then(onComplete: () => mixed): void,
+ commit(): void,
+
+ _reactRootContainer: FiberRoot,
+
+ _completionCallbacks: Array<() => mixed> | null,
+ _didComplete: boolean,
+};
+
+function insertWork(root: FiberRoot, work: FiberRootWorkNode) {
+ // Insert into root's list of work nodes.
+ const expirationTime = work._expirationTime;
+ const firstNode = root.firstTopLevelWork;
+ if (firstNode === null) {
+ root.firstTopLevelWork = work;
+ work._next = null;
+ } else {
+ // Insert sorted by expiration time then insertion order
+ let insertAfter = null;
+ let insertBefore = firstNode;
+ while (
+ insertBefore !== null &&
+ insertBefore._expirationTime <= expirationTime
+ ) {
+ insertAfter = insertBefore;
+ insertBefore = insertBefore._next;
+ }
+ work._next = insertBefore;
+ if (insertAfter !== null) {
+ insertAfter._next = work;
+ }
+ }
+}
+
+function Work(root: FiberRoot, defer: boolean, expirationTime: ExpirationTime) {
+ this._reactRootContainer = root;
+
+ this._defer = defer;
+ this._expirationTime = expirationTime;
+ this._next = null;
+
+ this._completionCallbacks = null;
+ this._didComplete = false;
+}
+Work.prototype._onComplete = function() {
+ this._didComplete = true;
+ // Set this to null so it isn't called again.
+ this._onComplete = null;
+ const callbacks = this._completionCallbacks;
+ if (callbacks === null) {
+ return;
+ }
+ for (let i = 0; i < callbacks.length; i++) {
+ // TODO: Error handling
+ const callback = callbacks[i];
+ callback();
+ }
+};
+Work.prototype.then = function(cb: () => mixed) {
+ if (this._didComplete) {
+ cb();
+ return;
+ }
+ let completionCallbacks = this._completionCallbacks;
+ if (completionCallbacks === null) {
+ completionCallbacks = this._completionCallbacks = [];
+ }
+ completionCallbacks.push(cb);
+
+ const root = this._reactRootContainer;
+ const expirationTime = this._expirationTime;
+ DOMRenderer.requestWork(root, expirationTime);
+};
+Work.prototype.commit = function() {
+ this._defer = false;
+ const root = this._reactRootContainer;
+ const expirationTime = this._expirationTime;
+ DOMRenderer.flushRoot(root, expirationTime);
+};
+
type ReactRootNode = {
- render(children: ReactNodeList, callback: ?() => mixed): void,
- unmount(callback: ?() => mixed): void,
+ render(children: ReactNodeList, callback: ?() => mixed): WorkNode,
+ unmount(callback: ?() => mixed): WorkNode,
_reactRootContainer: *,
};
@@ -777,13 +869,51 @@ function ReactRoot(container: Container, hydrate: boolean) {
ReactRoot.prototype.render = function(
children: ReactNodeList,
callback: ?() => mixed,
-): void {
+): WorkNode {
+ const root = this._reactRootContainer;
+ // TODO: Wrapping in batchedUpdates is needed to prevent work on the root from
+ // starting until after the work object is inserted. Remove it once
+ // root scheduling is lifted into the renderer.
+ return DOMRenderer.batchedUpdates(() => {
+ const expirationTime = DOMRenderer.updateContainer(
+ children,
+ root,
+ null,
+ null,
+ );
+ const work = new Work(root, false, expirationTime);
+ insertWork(root, work);
+ return work;
+ });
+};
+ReactRoot.prototype.prerender = function(children: ReactNodeList): WorkNode {
const root = this._reactRootContainer;
- DOMRenderer.updateContainer(children, root, null, callback);
+ // TODO: Wrapping in batchedUpdates is needed to prevent work on the root from
+ // starting until after the work object is inserted. Remove it once
+ // root scheduling is lifted into the renderer.
+ return DOMRenderer.batchedUpdates(() => {
+ const expirationTime = DOMRenderer.updateContainer(
+ children,
+ root,
+ null,
+ null,
+ );
+ const work = new Work(root, true, expirationTime);
+ insertWork(root, work);
+ return work;
+ });
};
-ReactRoot.prototype.unmount = function(callback) {
+ReactRoot.prototype.unmount = function(): WorkNode {
const root = this._reactRootContainer;
- DOMRenderer.updateContainer(null, root, null, callback);
+ // TODO: Wrapping in batchedUpdates is needed to prevent work on the root from
+ // starting until after the work object is inserted. Remove it once
+ // root scheduling is lifted into the renderer.
+ return DOMRenderer.batchedUpdates(() => {
+ const expirationTime = DOMRenderer.updateContainer(null, root, null, null);
+ const work = new Work(root, false, expirationTime);
+ insertWork(root, work);
+ return work;
+ });
};
var ReactDOM = {
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index efeb2a29f28..9efc9d097d9 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -47,7 +47,7 @@ var {
reconcileChildFibersInPlace,
cloneChildFibers,
} = require('./ReactChildFiber');
-var {processUpdateQueue} = require('./ReactFiberUpdateQueue');
+var {processFiberUpdateQueue} = require('./ReactFiberUpdateQueue');
var {
getMaskedContext,
getUnmaskedContext,
@@ -323,7 +323,7 @@ module.exports = function(
const updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
const prevState = workInProgress.memoizedState;
- const state = processUpdateQueue(
+ const state = processFiberUpdateQueue(
current,
workInProgress,
updateQueue,
@@ -714,7 +714,7 @@ module.exports = function(
function memoizeState(workInProgress: Fiber, nextState: any) {
workInProgress.memoizedState = nextState;
// Don't reset the updateQueue, in case there are pending updates. Resetting
- // is handled by processUpdateQueue.
+ // is handled by processFiberUpdateQueue.
}
function beginWork(
diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js
index 396be01fb8a..a736c8a5d21 100644
--- a/packages/react-reconciler/src/ReactFiberClassComponent.js
+++ b/packages/react-reconciler/src/ReactFiberClassComponent.js
@@ -30,7 +30,7 @@ var {
} = require('./ReactFiberContext');
var {
insertUpdateIntoFiber,
- processUpdateQueue,
+ processFiberUpdateQueue,
} = require('./ReactFiberUpdateQueue');
var {hasContextChanged} = require('./ReactFiberContext');
@@ -448,7 +448,7 @@ module.exports = function(
// process them now.
const updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
- instance.state = processUpdateQueue(
+ instance.state = processFiberUpdateQueue(
current,
workInProgress,
updateQueue,
@@ -505,7 +505,7 @@ module.exports = function(
// // Process the update queue before calling shouldComponentUpdate
// const updateQueue = workInProgress.updateQueue;
// if (updateQueue !== null) {
- // newState = processUpdateQueue(
+ // newState = processFiberUpdateQueue(
// workInProgress,
// updateQueue,
// instance,
@@ -548,7 +548,7 @@ module.exports = function(
// // componentWillMount may have called setState. Process the update queue.
// const newUpdateQueue = workInProgress.updateQueue;
// if (newUpdateQueue !== null) {
- // newState = processUpdateQueue(
+ // newState = processFiberUpdateQueue(
// workInProgress,
// newUpdateQueue,
// instance,
@@ -614,7 +614,7 @@ module.exports = function(
// TODO: Previous state can be null.
let newState;
if (workInProgress.updateQueue !== null) {
- newState = processUpdateQueue(
+ newState = processFiberUpdateQueue(
current,
workInProgress,
workInProgress.updateQueue,
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js
index b8d1377a512..05719d404a9 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.js
@@ -12,6 +12,7 @@
import type {Fiber} from './ReactFiber';
import type {FiberRoot} from './ReactFiberRoot';
import type {ReactNodeList} from 'shared/ReactTypes';
+import type {ExpirationTime} from './ReactFiberExpirationTime';
var ReactFeatureFlags = require('shared/ReactFeatureFlags');
var ReactInstanceMap = require('shared/ReactInstanceMap');
@@ -223,7 +224,9 @@ export type Reconciler = {
container: OpaqueRoot,
parentComponent: ?React$Component,
callback: ?Function,
- ): void,
+ ): ExpirationTime,
+ flushRoot(root: OpaqueRoot, expirationTime: ExpirationTime): void,
+ requestWork(root: OpaqueRoot, expirationTime: ExpirationTime): void,
batchedUpdates(fn: () => A): A,
unbatchedUpdates(fn: () => A): A,
flushSync(fn: () => A): A,
@@ -264,6 +267,8 @@ module.exports = function(
computeAsyncExpiration,
computeExpirationForFiber,
scheduleWork,
+ requestWork,
+ flushRoot,
batchedUpdates,
unbatchedUpdates,
flushSync,
@@ -325,11 +330,12 @@ module.exports = function(
callback,
isReplace: false,
isForced: false,
- nextCallback: null,
next: null,
};
insertUpdateIntoFiber(current, update);
scheduleWork(current, expirationTime);
+
+ return expirationTime;
}
return {
@@ -342,7 +348,7 @@ module.exports = function(
container: OpaqueRoot,
parentComponent: ?React$Component,
callback: ?Function,
- ): void {
+ ): ExpirationTime {
// TODO: If this is a nested container, this won't be the root.
const current = container.current;
@@ -365,9 +371,13 @@ module.exports = function(
container.pendingContext = context;
}
- scheduleTopLevelUpdate(current, element, callback);
+ return scheduleTopLevelUpdate(current, element, callback);
},
+ flushRoot,
+
+ requestWork,
+
batchedUpdates,
unbatchedUpdates,
diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js
index a8fa67e49b9..2e519f1b92c 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.js
@@ -15,6 +15,14 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
const {createHostRootFiber} = require('./ReactFiber');
const {NoWork} = require('./ReactFiberExpirationTime');
+// TODO: This should be lifted into the renderer.
+export type WorkNode = {
+ _defer: boolean,
+ _expirationTime: ExpirationTime,
+ _onComplete: () => mixed,
+ _next: WorkNode | null,
+};
+
export type FiberRoot = {
// Any additional information from the host associated with this root.
containerInfo: any,
@@ -36,6 +44,10 @@ export type FiberRoot = {
pendingContext: Object | null,
// Determines if we should attempt to hydrate on the initial mount
+hydrate: boolean,
+ // List of top-level work nodes. This list indicates whether a commit should
+ // be deferred. Also contains completion callbacks.
+ // TODO: Lift this into the renderer
+ firstTopLevelWork: WorkNode | null,
// Linked-list of roots
nextScheduledRoot: FiberRoot | null,
};
@@ -57,6 +69,7 @@ exports.createFiberRoot = function(
context: null,
pendingContext: null,
hydrate,
+ firstTopLevelWork: null,
nextScheduledRoot: null,
};
uninitializedFiber.stateNode = root;
diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js
index 67a87c9a4a3..26a4b68250a 100644
--- a/packages/react-reconciler/src/ReactFiberScheduler.js
+++ b/packages/react-reconciler/src/ReactFiberScheduler.js
@@ -11,7 +11,7 @@
import type {HostConfig, Deadline} from 'react-reconciler';
import type {Fiber} from './ReactFiber';
-import type {FiberRoot} from './ReactFiberRoot';
+import type {FiberRoot, WorkNode} from './ReactFiberRoot';
import type {HydrationContext} from './ReactFiberHydrationContext';
import type {ExpirationTime} from './ReactFiberExpirationTime';
@@ -62,7 +62,7 @@ var {
computeExpirationBucket,
} = require('./ReactFiberExpirationTime');
var {AsyncUpdates} = require('./ReactTypeOfInternalContext');
-var {getUpdateExpirationTime} = require('./ReactFiberUpdateQueue');
+var {getFiberUpdateQueueExpirationTime} = require('./ReactFiberUpdateQueue');
var {resetContext} = require('./ReactFiberContext');
export type CapturedError = {
@@ -504,7 +504,7 @@ module.exports = function(
}
// Check for pending updates.
- let newExpirationTime = getUpdateExpirationTime(workInProgress);
+ let newExpirationTime = getFiberUpdateQueueExpirationTime(workInProgress);
// TODO: Calls need to visit stateNode
@@ -1289,6 +1289,8 @@ module.exports = function(
let isBatchingUpdates: boolean = false;
let isUnbatchingUpdates: boolean = false;
+ let completionCallbacks: Array | null = null;
+
// Use these to prevent an infinite loop of nested updates
const NESTED_UPDATE_LIMIT = 1000;
let nestedUpdateCount: number = 0;
@@ -1344,7 +1346,7 @@ module.exports = function(
if (isUnbatchingUpdates) {
// ...unless we're inside unbatchedUpdates, in which case we should
// flush it now.
- performWorkOnRoot(root, Sync);
+ performWorkOnRoot(root, Sync, recalculateCurrentTime());
}
return;
}
@@ -1451,7 +1453,11 @@ module.exports = function(
nextFlushedExpirationTime <= minExpirationTime) &&
!deadlineDidExpire
) {
- performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime);
+ performWorkOnRoot(
+ nextFlushedRoot,
+ nextFlushedExpirationTime,
+ recalculateCurrentTime(),
+ );
// Find the next highest priority work.
findHighestPriorityRoot();
}
@@ -1474,6 +1480,38 @@ module.exports = function(
deadlineDidExpire = false;
nestedUpdateCount = 0;
+ finishRendering();
+ }
+
+ function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
+ invariant(
+ !isRendering,
+ 'work.commit(): Cannot commit while already rendering. This likely ' +
+ 'means you attempted to commit from inside a lifecycle method.',
+ );
+ // Perform work on root as if the given expiration time is the current time.
+ // This has the effect of synchronously flushing all work up to and
+ // including the given time.
+ performWorkOnRoot(root, expirationTime, expirationTime);
+ finishRendering();
+ }
+
+ function finishRendering() {
+ if (completionCallbacks !== null) {
+ const callbackNodes = completionCallbacks;
+ completionCallbacks = null;
+ for (let i = 0; i < callbackNodes.length; i++) {
+ // This node might be processed again. Clear the callback so it's
+ // only called once.
+ const callbackNode = callbackNodes[i];
+ const callback = callbackNode._onComplete;
+ if (typeof callback === 'function') {
+ // TODO: How should errors be handled?
+ callback.call(callbackNode);
+ }
+ }
+ }
+
if (hasUnhandledError) {
const error = unhandledError;
unhandledError = null;
@@ -1482,7 +1520,11 @@ module.exports = function(
}
}
- function performWorkOnRoot(root, expirationTime) {
+ function performWorkOnRoot(
+ root: FiberRoot,
+ expirationTime: ExpirationTime,
+ currentTime: ExpirationTime,
+ ) {
invariant(
!isRendering,
'performWorkOnRoot was called recursively. This error is likely caused ' +
@@ -1492,20 +1534,18 @@ module.exports = function(
isRendering = true;
// Check if this is async work or sync/expired work.
- // TODO: Pass current time as argument to renderRoot, commitRoot
- if (expirationTime <= recalculateCurrentTime()) {
+ if (expirationTime <= currentTime) {
// Flush sync work.
let finishedWork = root.finishedWork;
if (finishedWork !== null) {
// This root is already complete. We can commit it.
- root.finishedWork = null;
- root.remainingExpirationTime = commitRoot(finishedWork);
+ completeRoot(root, finishedWork, expirationTime);
} else {
root.finishedWork = null;
finishedWork = renderRoot(root, expirationTime);
if (finishedWork !== null) {
// We've completed the root. Commit it.
- root.remainingExpirationTime = commitRoot(finishedWork);
+ completeRoot(root, finishedWork, expirationTime);
}
}
} else {
@@ -1513,8 +1553,7 @@ module.exports = function(
let finishedWork = root.finishedWork;
if (finishedWork !== null) {
// This root is already complete. We can commit it.
- root.finishedWork = null;
- root.remainingExpirationTime = commitRoot(finishedWork);
+ completeRoot(root, finishedWork, expirationTime);
} else {
root.finishedWork = null;
finishedWork = renderRoot(root, expirationTime);
@@ -1523,7 +1562,7 @@ module.exports = function(
// before committing.
if (!shouldYield()) {
// Still time left. Commit the root.
- root.remainingExpirationTime = commitRoot(finishedWork);
+ completeRoot(root, finishedWork, expirationTime);
} else {
// There's no time left. Mark this root as complete. We'll come
// back and commit it later.
@@ -1536,6 +1575,51 @@ module.exports = function(
isRendering = false;
}
+ function completeRoot(
+ root: FiberRoot,
+ finishedWork: Fiber,
+ expirationTime: ExpirationTime,
+ ): void {
+ // If there are top-level work nodes, check if the commit is deferred by
+ // traversing the list.
+ let firstDeferredNode = null;
+ let firstIncompleteNode = root.firstTopLevelWork;
+ while (
+ firstIncompleteNode !== null &&
+ firstIncompleteNode._expirationTime <= expirationTime
+ ) {
+ if (firstIncompleteNode._defer && firstDeferredNode === null) {
+ // This node's expiration matches, but its commit is deferred. We're
+ // blocked from committing at this level.
+ firstDeferredNode = firstIncompleteNode;
+ }
+ if (typeof firstIncompleteNode._onComplete === 'function') {
+ // This node has a callback. Add it to the list of completion callbacks.
+ if (completionCallbacks === null) {
+ completionCallbacks = [];
+ }
+ completionCallbacks.push(firstIncompleteNode);
+ }
+ firstIncompleteNode = firstIncompleteNode._next;
+ }
+
+ // The new first node is the first one that can't be committed.
+ root.firstTopLevelWork = firstDeferredNode === null
+ ? firstIncompleteNode
+ : firstDeferredNode;
+
+ if (firstDeferredNode === null) {
+ // Commit the root.
+ root.finishedWork = null;
+ root.remainingExpirationTime = commitRoot(finishedWork);
+ } else {
+ // This root is not ready to commit. Unschedule it until we receive
+ // another update.
+ root.finishedWork = finishedWork;
+ root.remainingExpirationTime = NoWork;
+ }
+ }
+
// When working on async work, the reconciler asks the renderer if it should
// yield execution. For DOM, we implement this with requestIdleCallback.
function shouldYield() {
@@ -1617,6 +1701,8 @@ module.exports = function(
computeAsyncExpiration,
computeExpirationForFiber,
scheduleWork,
+ requestWork,
+ flushRoot,
batchedUpdates,
unbatchedUpdates,
flushSync,
diff --git a/packages/react-reconciler/src/ReactFiberUpdateQueue.js b/packages/react-reconciler/src/ReactFiberUpdateQueue.js
index 1fd6fa18cb8..60bcb590fab 100644
--- a/packages/react-reconciler/src/ReactFiberUpdateQueue.js
+++ b/packages/react-reconciler/src/ReactFiberUpdateQueue.js
@@ -82,6 +82,7 @@ function createUpdateQueue(baseState: State): UpdateQueue {
}
return queue;
}
+exports.createUpdateQueue = createUpdateQueue;
function insertUpdateIntoQueue(
queue: UpdateQueue,
@@ -165,7 +166,7 @@ function insertUpdateIntoFiber(
}
exports.insertUpdateIntoFiber = insertUpdateIntoFiber;
-function getUpdateExpirationTime(fiber: Fiber): ExpirationTime {
+function getFiberUpdateQueueExpirationTime(fiber: Fiber): ExpirationTime {
if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) {
return NoWork;
}
@@ -175,7 +176,7 @@ function getUpdateExpirationTime(fiber: Fiber): ExpirationTime {
}
return updateQueue.expirationTime;
}
-exports.getUpdateExpirationTime = getUpdateExpirationTime;
+exports.getFiberUpdateQueueExpirationTime = getFiberUpdateQueueExpirationTime;
function getStateFromUpdate(update, instance, prevState, props) {
const partialState = update.partialState;
@@ -188,29 +189,11 @@ function getStateFromUpdate(update, instance, prevState, props) {
}
function processUpdateQueue(
- current: Fiber | null,
- workInProgress: Fiber,
queue: UpdateQueue,
- instance: any,
+ context: any,
props: any,
renderExpirationTime: ExpirationTime,
): State {
- if (current !== null && current.updateQueue === queue) {
- // We need to create a work-in-progress queue, by cloning the current queue.
- const currentQueue = queue;
- queue = workInProgress.updateQueue = {
- baseState: currentQueue.baseState,
- expirationTime: currentQueue.expirationTime,
- first: currentQueue.first,
- last: currentQueue.last,
- isInitialized: currentQueue.isInitialized,
- // These fields are no longer valid because they were already committed.
- // Reset them.
- callbackList: null,
- hasForceUpdate: false,
- };
- }
-
if (__DEV__) {
// Set this flag so we can warn if setState is called inside the update
// function of another setState.
@@ -221,17 +204,7 @@ function processUpdateQueue(
// increase this accordingly.
queue.expirationTime = NoWork;
- // TODO: We don't know what the base state will be until we begin work.
- // It depends on which fiber is the next current. Initialize with an empty
- // base state, then set to the memoizedState when rendering. Not super
- // happy with this approach.
- let state;
- if (queue.isInitialized) {
- state = queue.baseState;
- } else {
- state = queue.baseState = workInProgress.memoizedState;
- queue.isInitialized = true;
- }
+ let state = queue.baseState;
let dontMutatePrevState = true;
let update = queue.first;
let didSkip = false;
@@ -270,10 +243,10 @@ function processUpdateQueue(
// Process the update
let partialState;
if (update.isReplace) {
- state = getStateFromUpdate(update, instance, state, props);
+ state = getStateFromUpdate(update, context, state, props);
dontMutatePrevState = true;
} else {
- partialState = getStateFromUpdate(update, instance, state, props);
+ partialState = getStateFromUpdate(update, context, state, props);
if (partialState) {
if (dontMutatePrevState) {
// $FlowFixMe: Idk how to type this properly.
@@ -298,13 +271,6 @@ function processUpdateQueue(
update = update.next;
}
- if (queue.callbackList !== null) {
- workInProgress.effectTag |= CallbackEffect;
- } else if (queue.first === null && !queue.hasForceUpdate) {
- // The queue is empty. We can reset it.
- workInProgress.updateQueue = null;
- }
-
if (!didSkip) {
didSkip = true;
queue.baseState = state;
@@ -319,6 +285,57 @@ function processUpdateQueue(
}
exports.processUpdateQueue = processUpdateQueue;
+function processFiberUpdateQueue(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ queue: UpdateQueue,
+ instance: any,
+ props: any,
+ renderExpirationTime: ExpirationTime,
+): State {
+ if (current !== null && current.updateQueue === queue) {
+ // We need to create a work-in-progress queue, by cloning the current queue.
+ const currentQueue = queue;
+ queue = workInProgress.updateQueue = {
+ baseState: currentQueue.baseState,
+ expirationTime: currentQueue.expirationTime,
+ first: currentQueue.first,
+ last: currentQueue.last,
+ isInitialized: currentQueue.isInitialized,
+ // These fields are no longer valid because they were already committed.
+ // Reset them.
+ callbackList: null,
+ hasForceUpdate: false,
+ };
+ }
+
+ // TODO: We don't know what the base state will be until we begin work.
+ // It depends on which fiber is the next current. Initialize with an empty
+ // base state, then set to the memoizedState when rendering. Not super
+ // happy with this approach.
+ if (!queue.isInitialized) {
+ queue.baseState = workInProgress.memoizedState;
+ queue.isInitialized = true;
+ }
+
+ const state = processUpdateQueue(
+ queue,
+ instance,
+ props,
+ renderExpirationTime,
+ );
+
+ if (queue.callbackList !== null) {
+ workInProgress.effectTag |= CallbackEffect;
+ } else if (queue.first === null && !queue.hasForceUpdate) {
+ // The queue is empty. We can reset it.
+ workInProgress.updateQueue = null;
+ }
+
+ return state;
+}
+exports.processFiberUpdateQueue = processFiberUpdateQueue;
+
function commitCallbacks(queue: UpdateQueue, context: any) {
const callbackList = queue.callbackList;
if (callbackList === null) {
diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js
index 4e379f21575..3f8ea24cf83 100644
--- a/packages/react-test-renderer/src/ReactTestRenderer.js
+++ b/packages/react-test-renderer/src/ReactTestRenderer.js
@@ -607,7 +607,7 @@ var ReactTestRendererFiber = {
if (root == null || root.current == null) {
return;
}
- TestRenderer.updateContainer(null, root, null);
+ TestRenderer.updateContainer(null, root, null, null);
container = null;
root = null;
},