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; },