diff --git a/src/isomorphic/classic/class/ReactClass.js b/src/isomorphic/classic/class/ReactClass.js index 475f08d7d81b2..f0a59650062b8 100644 --- a/src/isomorphic/classic/class/ReactClass.js +++ b/src/isomorphic/classic/class/ReactClass.js @@ -728,6 +728,11 @@ var ReactClassMixin = { * type signature and the only use case for this, is to avoid that. */ replaceState: function(newState, callback) { + if (this.updater.isFiberUpdater) { + this.updater.enqueueReplaceState(this, newState, callback); + return; + } + this.updater.enqueueReplaceState(this, newState); if (callback) { this.updater.enqueueCallback(this, callback, 'replaceState'); diff --git a/src/isomorphic/modern/class/ReactComponent.js b/src/isomorphic/modern/class/ReactComponent.js index b93da63e94cf9..1eac5e786dee5 100644 --- a/src/isomorphic/modern/class/ReactComponent.js +++ b/src/isomorphic/modern/class/ReactComponent.js @@ -65,6 +65,12 @@ ReactComponent.prototype.setState = function(partialState, callback) { 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.' ); + + if (this.updater.isFiberUpdater) { + this.updater.enqueueSetState(this, partialState, callback); + return; + } + this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); @@ -86,6 +92,11 @@ ReactComponent.prototype.setState = function(partialState, callback) { * @protected */ ReactComponent.prototype.forceUpdate = function(callback) { + if (this.updater.isFiberUpdater) { + this.updater.enqueueForceUpdate(this, callback); + return; + } + this.updater.enqueueForceUpdate(this); if (callback) { this.updater.enqueueCallback(this, callback, 'forceUpdate'); diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index 8294883db55bc..08fd6d7c59df8 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -112,6 +112,8 @@ var DOMRenderer = ReactFiberReconciler({ scheduleDeferredCallback: window.requestIdleCallback, + useSyncScheduling: true, + }); var warned = false; @@ -163,6 +165,10 @@ var ReactDOM = { return DOMRenderer.findHostInstance(component); }, + unstable_batchedUpdates(fn : () => A) : A { + return DOMRenderer.batchedUpdates(fn); + }, + }; module.exports = ReactDOM; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index bcc8958e54222..d8b41f77378cf 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -48,7 +48,7 @@ var ReactFiberClassComponent = require('ReactFiberClassComponent'); module.exports = function( config : HostConfig, - scheduleUpdate : (fiber: Fiber, priorityLevel : PriorityLevel) => void + scheduleUpdate : (fiber: Fiber, priorityLevel : ?PriorityLevel) => void ) { const { diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 2211dead1876f..ac07fc852cdf6 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -16,7 +16,6 @@ import type { Fiber } from 'ReactFiber'; import type { PriorityLevel } from 'ReactPriorityLevel'; import type { UpdateQueue } from 'ReactFiberUpdateQueue'; -var { LowPriority } = require('ReactPriorityLevel'); var { createUpdateQueue, addToQueue, @@ -27,51 +26,50 @@ var { isMounted } = require('ReactFiberTreeReflection'); var ReactInstanceMap = require('ReactInstanceMap'); var shallowEqual = require('shallowEqual'); -module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : PriorityLevel) => void) { +module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : ?PriorityLevel) => void) { - function scheduleUpdateQueue(fiber: Fiber, updateQueue: UpdateQueue, priorityLevel : PriorityLevel) { + function scheduleUpdateQueue(fiber: Fiber, updateQueue: UpdateQueue) { fiber.updateQueue = updateQueue; // Schedule update on the alternate as well, since we don't know which tree // is current. if (fiber.alternate) { fiber.alternate.updateQueue = updateQueue; } - scheduleUpdate(fiber, priorityLevel); + scheduleUpdate(fiber); } // Class component state updater const updater = { isMounted, - enqueueSetState(instance, partialState) { + enqueueSetState(instance, partialState, callback) { const fiber = ReactInstanceMap.get(instance); const updateQueue = fiber.updateQueue ? addToQueue(fiber.updateQueue, partialState) : createUpdateQueue(partialState); - scheduleUpdateQueue(fiber, updateQueue, LowPriority); + if (callback) { + addCallbackToQueue(updateQueue, callback); + } + scheduleUpdateQueue(fiber, updateQueue); }, - enqueueReplaceState(instance, state) { + enqueueReplaceState(instance, state, callback) { const fiber = ReactInstanceMap.get(instance); const updateQueue = createUpdateQueue(state); updateQueue.isReplace = true; - scheduleUpdateQueue(fiber, updateQueue, LowPriority); + if (callback) { + addCallbackToQueue(updateQueue, callback); + } + scheduleUpdateQueue(fiber, updateQueue); }, - enqueueForceUpdate(instance) { + enqueueForceUpdate(instance, callback) { const fiber = ReactInstanceMap.get(instance); const updateQueue = fiber.updateQueue || createUpdateQueue(null); updateQueue.isForced = true; - scheduleUpdateQueue(fiber, updateQueue, LowPriority); - }, - enqueueCallback(instance, callback) { - const fiber = ReactInstanceMap.get(instance); - let updateQueue = fiber.updateQueue ? - fiber.updateQueue : - createUpdateQueue(null); - addCallbackToQueue(updateQueue, callback); - fiber.updateQueue = updateQueue; - if (fiber.alternate) { - fiber.alternate.updateQueue = updateQueue; + if (callback) { + addCallbackToQueue(updateQueue, callback); } + scheduleUpdateQueue(fiber, updateQueue); }, + isFiberUpdater: true, }; function checkShouldComponentUpdate(workInProgress, oldProps, newProps, newState) { @@ -131,7 +129,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori // process them now. const updateQueue = workInProgress.updateQueue; if (updateQueue) { - instance.state = mergeUpdateQueue(updateQueue, state, props); + instance.state = mergeUpdateQueue(updateQueue, instance, state, props); } } } @@ -175,7 +173,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori // process them now. const newUpdateQueue = workInProgress.updateQueue; if (newUpdateQueue) { - newInstance.state = mergeUpdateQueue(newUpdateQueue, newState, newProps); + newInstance.state = mergeUpdateQueue(newUpdateQueue, newInstance, newState, newProps); } } return true; @@ -212,7 +210,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori // TODO: Previous state can be null. let newState; if (updateQueue) { - newState = mergeUpdateQueue(updateQueue, previousState, newProps); + newState = mergeUpdateQueue(updateQueue, instance, previousState, newProps); } else { newState = previousState; } diff --git a/src/renderers/shared/fiber/ReactFiberErrorBoundary.js b/src/renderers/shared/fiber/ReactFiberErrorBoundary.js index 7268882155963..f064c7f3f6423 100644 --- a/src/renderers/shared/fiber/ReactFiberErrorBoundary.js +++ b/src/renderers/shared/fiber/ReactFiberErrorBoundary.js @@ -13,14 +13,17 @@ 'use strict'; import type { Fiber } from 'ReactFiber'; +import type { FiberRoot } from 'ReactFiberRoot'; var { ClassComponent, + HostContainer, } = require('ReactTypeOfWork'); export type TrappedError = { boundary: Fiber | null, error: any, + root: FiberRoot, }; function findClosestErrorBoundary(fiber : Fiber): Fiber | null { @@ -37,9 +40,24 @@ function findClosestErrorBoundary(fiber : Fiber): Fiber | null { return null; } +function findRoot(fiber : Fiber) : FiberRoot { + while (fiber) { + if (!fiber.return) { + if (fiber.tag === HostContainer) { + return ((fiber.stateNode : any) : FiberRoot); + } else { + throw new Error('Invalid root'); + } + } + fiber = fiber.return; + } + throw new Error('Could not find a root.'); +} + function trapError(fiber : Fiber, error : any) : TrappedError { return { boundary: findClosestErrorBoundary(fiber), + root: findRoot(fiber), error, }; } diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index d5995a3c65b01..fd7fc1b586225 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -28,7 +28,7 @@ if (__DEV__) { var { findCurrentHostFiber } = require('ReactFiberTreeReflection'); -type Deadline = { +export type Deadline = { timeRemaining : () => number }; @@ -56,8 +56,9 @@ export type HostConfig = { removeChild(parentInstance : I, child : I | TI) : void, scheduleAnimationCallback(callback : () => void) : void, - scheduleDeferredCallback(callback : (deadline : Deadline) => void) : void + scheduleDeferredCallback(callback : (deadline : Deadline) => void) : void, + useSyncScheduling ?: boolean, }; type OpaqueNode = Fiber; @@ -67,6 +68,10 @@ export type Reconciler = { updateContainer(element : ReactElement, container : OpaqueNode) : void, unmountContainer(container : OpaqueNode) : void, performWithPriority(priorityLevel : PriorityLevel, fn : Function) : void, + /* eslint-disable no-undef */ + // FIXME: ESLint complains about type parameter + batchedUpdates(fn : () => A) : A, + /* eslint-enable no-undef */ // Used to extract the return value from the initial render. Legacy API. getPublicRootInstance(container : OpaqueNode) : (ReactComponent | TI | I | null), @@ -77,7 +82,11 @@ export type Reconciler = { module.exports = function(config : HostConfig) : Reconciler { - var { scheduleWork, performWithPriority } = ReactFiberScheduler(config); + var { + scheduleWork, + performWithPriority, + batchedUpdates, + } = ReactFiberScheduler(config); return { @@ -141,6 +150,8 @@ module.exports = function(config : HostConfig) : performWithPriority, + batchedUpdates, + getPublicRootInstance(container : OpaqueNode) : (ReactComponent | I | TI | null) { const root : FiberRoot = (container.stateNode : any); const containerFiber = root.current; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 4c7b0c515422a..e4a04eb478203 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -15,7 +15,7 @@ import type { TrappedError } from 'ReactFiberErrorBoundary'; import type { Fiber } from 'ReactFiber'; import type { FiberRoot } from 'ReactFiberRoot'; -import type { HostConfig } from 'ReactFiberReconciler'; +import type { HostConfig, Deadline } from 'ReactFiberReconciler'; import type { PriorityLevel } from 'ReactPriorityLevel'; var ReactFiberBeginWork = require('ReactFiberBeginWork'); @@ -62,19 +62,33 @@ module.exports = function(config : HostConfig) { const scheduleAnimationCallback = config.scheduleAnimationCallback; const scheduleDeferredCallback = config.scheduleDeferredCallback; + const useSyncScheduling = config.useSyncScheduling; - // The default priority to use for updates. - let defaultPriority : PriorityLevel = LowPriority; + // The priority level to use when scheduling an update. + let priorityContext : PriorityLevel = useSyncScheduling ? + SynchronousPriority : + LowPriority; + + // Whether updates should be batched. Only applies when using sync scheduling. + let shouldBatchUpdates : boolean = false; // The next work in progress fiber that we're currently working on. let nextUnitOfWork : ?Fiber = null; let nextPriorityLevel : PriorityLevel = NoWork; + // The next errors we need to handle before the next item of work. + let nextTrappedErrors : Array | null = null; + // Linked list of roots with scheduled work on them. let nextScheduledRoot : ?FiberRoot = null; let lastScheduledRoot : ?FiberRoot = null; function findNextUnitOfWork() { + if (nextTrappedErrors) { + // While we have errors, we can't perform any normal work. + return null; + } + // Clear out roots with no more work on them. while (nextScheduledRoot && nextScheduledRoot.current.pendingWorkPriority === NoWork) { nextScheduledRoot.isScheduled = false; @@ -204,10 +218,7 @@ module.exports = function(config : HostConfig) { } } - // Now that the tree has been committed, we can handle errors. - if (allTrappedErrors) { - handleErrors(allTrappedErrors); - } + nextTrappedErrors = allTrappedErrors; } function resetWorkPriority(workInProgress : Fiber) { @@ -365,20 +376,7 @@ module.exports = function(config : HostConfig) { } function performDeferredWork(deadline) { - try { - performDeferredWorkUnsafe(deadline); - } catch (error) { - const failedUnitOfWork = nextUnitOfWork; - // Reset because it points to the error boundary: - nextUnitOfWork = null; - if (!failedUnitOfWork) { - // We shouldn't end up here because nextUnitOfWork - // should always be set while work is being performed. - throw error; - } - const trappedError = trapError(failedUnitOfWork, error); - handleErrors([trappedError]); - } + performAndHandleErrors(LowPriority, deadline); } function scheduleDeferredWork(root : FiberRoot, priority : PriorityLevel) { @@ -417,35 +415,21 @@ module.exports = function(config : HostConfig) { // Always start from the root nextUnitOfWork = findNextUnitOfWork(); while (nextUnitOfWork && - nextPriorityLevel !== NoWork) { + nextPriorityLevel !== NoWork && + nextPriorityLevel <= AnimationPriority) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false); if (!nextUnitOfWork) { // Keep searching for animation work until there's no more left nextUnitOfWork = findNextUnitOfWork(); } - // Stop if the next unit of work is low priority - if (nextPriorityLevel > AnimationPriority) { - scheduleDeferredCallback(performDeferredWork); - return; - } + } + if (nextUnitOfWork && nextPriorityLevel > AnimationPriority) { + scheduleDeferredCallback(performDeferredWork); } } function performAnimationWork() { - try { - performAnimationWorkUnsafe(); - } catch (error) { - const failedUnitOfWork = nextUnitOfWork; - // Reset because it points to the error boundary: - nextUnitOfWork = null; - if (!failedUnitOfWork) { - // We shouldn't end up here because nextUnitOfWork - // should always be set while work is being performed. - throw error; - } - const trappedError = trapError(failedUnitOfWork, error); - handleErrors([trappedError]); - } + performAndHandleErrors(AnimationPriority); } function scheduleAnimationWork(root: FiberRoot, priorityLevel : PriorityLevel) { @@ -472,35 +456,107 @@ module.exports = function(config : HostConfig) { } } - function scheduleErrorBoundaryWork(boundary : Fiber, priority) : FiberRoot { - let root = null; + function scheduleErrorBoundaryWork(boundary : Fiber, priority) { let fiber = boundary; while (fiber) { fiber.pendingWorkPriority = priority; if (fiber.alternate) { fiber.alternate.pendingWorkPriority = priority; } - if (!fiber.return) { - if (fiber.tag === HostContainer) { - // We found the root. - // Remember it so we can update it. - root = ((fiber.stateNode : any) : FiberRoot); - break; + fiber = fiber.return; + } + } + + function performSynchronousWorkUnsafe() { + // Perform work now + nextUnitOfWork = findNextUnitOfWork(); + while (nextUnitOfWork && + nextPriorityLevel === SynchronousPriority) { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false); + + if (!nextUnitOfWork && shouldBatchUpdates) { + nextUnitOfWork = findNextUnitOfWork(); + } + } + if (nextUnitOfWork) { + if (nextPriorityLevel > AnimationPriority) { + scheduleDeferredCallback(performDeferredWork); + return; + } + scheduleAnimationCallback(performAnimationWork); + } + } + + function performSynchronousWork() { + const prev = shouldBatchUpdates; + shouldBatchUpdates = true; + // All nested updates are batched + try { + performAndHandleErrors(SynchronousPriority); + } finally { + shouldBatchUpdates = prev; + } + } + + function scheduleSynchronousWork(root : FiberRoot) { + root.current.pendingWorkPriority = SynchronousPriority; + + if (root.isScheduled) { + // If we're already scheduled, we can bail out. + return; + } + 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 (!shouldBatchUpdates) { + // Unless in batched mode, perform work immediately + performSynchronousWork(); + } + } + } + + function performAndHandleErrors(priorityLevel : PriorityLevel, deadline : ?Deadline) { + // The exact priority level doesn't matter, so long as it's in range of the + // work (sync, animation, deferred) being performed. + try { + if (priorityLevel === SynchronousPriority) { + performSynchronousWorkUnsafe(); + } else if (priorityLevel > AnimationPriority) { + if (!deadline) { + throw new Error('No deadline'); } else { - throw new Error('Invalid root'); + performDeferredWorkUnsafe(deadline); } + } else { + performAnimationWorkUnsafe(); } - fiber = fiber.return; + } catch (error) { + const failedUnitOfWork = nextUnitOfWork; + // Reset because it points to the error boundary: + nextUnitOfWork = null; + if (!failedUnitOfWork) { + // We shouldn't end up here because nextUnitOfWork + // should always be set while work is being performed. + throw error; + } + const trappedError = trapError(failedUnitOfWork, error); + nextTrappedErrors = [trappedError]; } - if (!root) { - throw new Error('Could not find root from the boundary.'); + if (nextTrappedErrors) { + handleErrors(); } - return root; } - function handleErrors(initialTrappedErrors : Array) : void { - let nextTrappedErrors = initialTrappedErrors; + function handleErrors() : void { let firstUncaughtError = null; + let rootsWithUncaughtErrors = null; // In each phase, we will attempt to pass errors to boundaries and re-render them. // If we get more errors, we propagate them to higher boundaries in the next iterations. @@ -509,22 +565,25 @@ module.exports = function(config : HostConfig) { nextTrappedErrors = null; // Pass errors to all affected boundaries. - const affectedBoundaries : Set = new Set(); + const affectedRootsByBondary : Map = new Map(); trappedErrors.forEach(trappedError => { const boundary = trappedError.boundary; const error = trappedError.error; + const root = trappedError.root; if (!boundary) { firstUncaughtError = firstUncaughtError || error; + rootsWithUncaughtErrors = rootsWithUncaughtErrors || new Set(); + rootsWithUncaughtErrors.add(root); return; } // Don't visit boundaries twice. - if (affectedBoundaries.has(boundary)) { + if (affectedRootsByBondary.has(boundary)) { return; } // Give error boundary a chance to update its state. try { acknowledgeErrorInBoundary(boundary, error); - affectedBoundaries.add(boundary); + affectedRootsByBondary.set(boundary, root); } catch (nextError) { // If it throws, propagate the error. nextTrappedErrors = nextTrappedErrors || []; @@ -533,18 +592,13 @@ module.exports = function(config : HostConfig) { }); // We will process an update caused by each error boundary synchronously. - affectedBoundaries.forEach(boundary => { - // FIXME: We only specify LowPriority here so that setState() calls from the error - // boundaries are respected. Instead we should set default priority level or something - // like this. Reconsider this piece when synchronous scheduling is in place. - const priority = LowPriority; - const root = scheduleErrorBoundaryWork(boundary, priority); + affectedRootsByBondary.forEach((root, boundary) => { + const priority = priorityContext; + scheduleErrorBoundaryWork(boundary, priority); // This should use findNextUnitOfWork() when synchronous scheduling is implemented. let fiber = cloneFiber(root.current, priority); try { while (fiber) { - // TODO: this is the only place where we recurse and it's unfortunate. - // (This may potentially get us into handleErrors() again.) fiber = performUnitOfWork(fiber, true); } } catch (nextError) { @@ -557,35 +611,69 @@ module.exports = function(config : HostConfig) { ReactCurrentOwner.current = null; + // Unschedule any roots with uncaught errors + if (rootsWithUncaughtErrors) { + // Filter the linked list + let newNextScheduledRoot = null; + let newLastScheduledRoot = null; + // Find first root that has no errors + let root = nextScheduledRoot; + while (root) { + if (rootsWithUncaughtErrors.has(root)) { + root.isScheduled = false; + root = root.nextScheduledRoot; + } else { + newNextScheduledRoot = root; + newLastScheduledRoot = root; + break; + } + } + // Find the rest of the roots with no errors + while (root) { + if (root.nextScheduledRoot && rootsWithUncaughtErrors.has(root.nextScheduledRoot)) { + root.nextScheduledRoot.isScheduled = false; + root.nextScheduledRoot = root.nextScheduledRoot.nextScheduledRoot; + } else { + newLastScheduledRoot = root; + root = root.nextScheduledRoot; + } + } + // Update the first and last pointers + nextScheduledRoot = newNextScheduledRoot; + lastScheduledRoot = newLastScheduledRoot; + } + // Surface the first error uncaught by the boundaries to the user. if (firstUncaughtError) { - // We need to make sure any future root can get scheduled despite these errors. - // Currently after throwing, nothing gets scheduled because these fields are set. - // FIXME: this is likely a wrong fix! It's still better than ignoring updates though. - nextScheduledRoot = null; - lastScheduledRoot = null; - - // Throw any unhandled errors. throw firstUncaughtError; } } - function scheduleWork(root : FiberRoot) { - if (defaultPriority === SynchronousPriority) { - throw new Error('Not implemented yet'); + function scheduleWork(root : FiberRoot, priorityLevel : ?PriorityLevel) { + if (priorityLevel == null) { + priorityLevel = priorityContext; } - if (defaultPriority === NoWork) { + if (priorityLevel === SynchronousPriority) { + scheduleSynchronousWork(root); + } + + if (priorityLevel === NoWork) { return; } - if (defaultPriority > AnimationPriority) { - scheduleDeferredWork(root, defaultPriority); + if (priorityLevel > AnimationPriority) { + scheduleDeferredWork(root, priorityLevel); return; } - scheduleAnimationWork(root, defaultPriority); + scheduleAnimationWork(root, priorityLevel); } - function scheduleUpdate(fiber: Fiber, priorityLevel : PriorityLevel): void { + function scheduleUpdate(fiber: Fiber, priorityLevel : ?PriorityLevel): void { + // Use priority context if no priority is provided + if (priorityLevel == null) { + priorityLevel = priorityContext; + } + while (true) { if (fiber.pendingWorkPriority === NoWork || fiber.pendingWorkPriority >= priorityLevel) { @@ -600,7 +688,7 @@ module.exports = function(config : HostConfig) { if (!fiber.return) { if (fiber.tag === HostContainer) { const root : FiberRoot = (fiber.stateNode : any); - scheduleDeferredWork(root, priorityLevel); + scheduleWork(root, priorityLevel); return; } else { throw new Error('Invalid root'); @@ -611,12 +699,26 @@ module.exports = function(config : HostConfig) { } function performWithPriority(priorityLevel : PriorityLevel, fn : Function) { - const previousDefaultPriority = defaultPriority; - defaultPriority = priorityLevel; + const previousPriorityContext = priorityContext; + priorityContext = priorityLevel; try { fn(); } finally { - defaultPriority = previousDefaultPriority; + priorityContext = previousPriorityContext; + } + } + + function batchedUpdates(fn : () => A) : A { + const prev = shouldBatchUpdates; + shouldBatchUpdates = true; + try { + return fn(); + } finally { + shouldBatchUpdates = prev; + // If we've exited the batch, perform any scheduled sync work + if (!shouldBatchUpdates) { + performSynchronousWork(); + } } } @@ -624,5 +726,6 @@ module.exports = function(config : HostConfig) { scheduleWork: scheduleWork, scheduleDeferredWork: scheduleDeferredWork, performWithPriority: performWithPriority, + batchedUpdates: batchedUpdates, }; }; diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index d352dd82c5f60..8e27e739811d7 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -73,14 +73,14 @@ exports.callCallbacks = function(queue : UpdateQueue, context : any) { } }; -exports.mergeUpdateQueue = function(queue : UpdateQueue, prevState : any, props : any) : any { +exports.mergeUpdateQueue = function(queue : UpdateQueue, instance : any, prevState : any, props : any) : any { let node : ?UpdateQueueNode = queue; let state = queue.isReplace ? null : Object.assign({}, prevState); while (node) { let partialState; if (typeof node.partialState === 'function') { const updateFn = node.partialState; - partialState = updateFn(state, props); + partialState = updateFn.call(instance, state, props); } else { partialState = node.partialState; } diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling.js new file mode 100644 index 0000000000000..17b9a9ab36d7c --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling.js @@ -0,0 +1,111 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails react-core + */ + +'use strict'; + +describe('ReactIncrementalErrorBoundaries', () => { + var React; + var ReactNoop; + var ErrorBoundary; + + beforeEach(() => { + React = require('React'); + ReactNoop = require('ReactNoop'); + }); + + it('can schedule updates after crashing in render on mount', () => { + var ops = []; + + function BrokenRender() { + ops.push('BrokenRender'); + throw new Error('Hello.'); + } + + function Foo() { + ops.push('Foo'); + return null; + } + + ReactNoop.render(); + expect(() => { + ReactNoop.flush(); + }).toThrow('Hello.'); + expect(ops).toEqual(['BrokenRender']); + + ops = []; + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual(['Foo']); + }); + + it('can schedule updates after crashing in render on update', () => { + var ops = []; + + function BrokenRender(props) { + ops.push('BrokenRender'); + if (props.throw) { + throw new Error('Hello.'); + } + } + + function Foo() { + ops.push('Foo'); + return null; + } + + ReactNoop.render(); + ReactNoop.flush(); + ops = []; + + expect(() => { + ReactNoop.render(); + ReactNoop.flush(); + }).toThrow('Hello.'); + expect(ops).toEqual(['BrokenRender']); + + ops = []; + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual(['Foo']); + }); + + it('can schedule updates after crashing during umounting', () => { + var ops = []; + + class BrokenComponentWillUnmount extends React.Component { + render() { + return
; + } + componentWillUnmount() { + throw new Error('Hello.'); + } + } + + function Foo() { + ops.push('Foo'); + return null; + } + + ReactNoop.render(); + ReactNoop.flush(); + + expect(() => { + ReactNoop.render(
); + ReactNoop.flush(); + }).toThrow('Hello.'); + + ops = []; + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual(['Foo']); + }); + +}); diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentState-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentState-test.js index c758aa5d2e665..5c6c34879d6b3 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentState-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentState-test.js @@ -11,6 +11,8 @@ 'use strict'; +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); + var React; var ReactDOM; @@ -148,72 +150,146 @@ describe('ReactCompositeComponent-state', () => { ReactDOM.unmountComponentAtNode(container); - expect(stateListener.mock.calls.join('\n')).toEqual([ - // there is no state when getInitialState() is called - ['getInitialState', null], - ['componentWillMount-start', 'red'], - // setState()'s only enqueue pending states. - ['componentWillMount-after-sunrise', 'red'], - ['componentWillMount-end', 'red'], - // pending state queue is processed - ['before-setState-sunrise', 'red'], - ['after-setState-sunrise', 'sunrise'], - ['after-setState-orange', 'orange'], - // pending state has been applied - ['render', 'orange'], - ['componentDidMount-start', 'orange'], - // setState-sunrise and setState-orange should be called here, - // after the bug in #1740 - // componentDidMount() called setState({color:'yellow'}), which is async. - // The update doesn't happen until the next flush. - ['componentDidMount-end', 'orange'], - ['shouldComponentUpdate-currentState', 'orange'], - ['shouldComponentUpdate-nextState', 'yellow'], - ['componentWillUpdate-currentState', 'orange'], - ['componentWillUpdate-nextState', 'yellow'], - ['render', 'yellow'], - ['componentDidUpdate-currentState', 'yellow'], - ['componentDidUpdate-prevState', 'orange'], - ['setState-sunrise', 'yellow'], - ['setState-orange', 'yellow'], - ['setState-yellow', 'yellow'], - ['initial-callback', 'yellow'], - ['componentWillReceiveProps-start', 'yellow'], - // setState({color:'green'}) only enqueues a pending state. - ['componentWillReceiveProps-end', 'yellow'], - // pending state queue is processed - // before-setState-receiveProps never called, due to replaceState. - ['before-setState-again-receiveProps', undefined], - ['after-setState-receiveProps', 'green'], - ['shouldComponentUpdate-currentState', 'yellow'], - ['shouldComponentUpdate-nextState', 'green'], - ['componentWillUpdate-currentState', 'yellow'], - ['componentWillUpdate-nextState', 'green'], - ['render', 'green'], - ['componentDidUpdate-currentState', 'green'], - ['componentDidUpdate-prevState', 'yellow'], - ['setState-receiveProps', 'green'], - ['setProps', 'green'], - // setFavoriteColor('blue') - ['shouldComponentUpdate-currentState', 'green'], - ['shouldComponentUpdate-nextState', 'blue'], - ['componentWillUpdate-currentState', 'green'], - ['componentWillUpdate-nextState', 'blue'], - ['render', 'blue'], - ['componentDidUpdate-currentState', 'blue'], - ['componentDidUpdate-prevState', 'green'], - ['setFavoriteColor', 'blue'], - // forceUpdate() - ['componentWillUpdate-currentState', 'blue'], - ['componentWillUpdate-nextState', 'blue'], - ['render', 'blue'], - ['componentDidUpdate-currentState', 'blue'], - ['componentDidUpdate-prevState', 'blue'], - ['forceUpdate', 'blue'], - // unmountComponent() - // state is available within `componentWillUnmount()` - ['componentWillUnmount', 'blue'], - ].join('\n')); + let expected; + if (ReactDOMFeatureFlags.useFiber) { + expected = [ + // there is no state when getInitialState() is called + ['getInitialState', null], + ['componentWillMount-start', 'red'], + // setState()'s only enqueue pending states. + ['componentWillMount-after-sunrise', 'red'], + ['componentWillMount-end', 'red'], + // pending state queue is processed + ['before-setState-sunrise', 'red'], + ['after-setState-sunrise', 'sunrise'], + ['after-setState-orange', 'orange'], + // pending state has been applied + ['render', 'orange'], + ['componentDidMount-start', 'orange'], + // setState-sunrise and setState-orange should be called here, + // after the bug in #1740 + // componentDidMount() called setState({color:'yellow'}), which is async. + // The update doesn't happen until the next flush. + ['componentDidMount-end', 'orange'], + ['setState-sunrise', 'orange'], + ['setState-orange', 'orange'], + ['initial-callback', 'orange'], + ['shouldComponentUpdate-currentState', 'orange'], + ['shouldComponentUpdate-nextState', 'yellow'], + ['componentWillUpdate-currentState', 'orange'], + ['componentWillUpdate-nextState', 'yellow'], + ['render', 'yellow'], + ['componentDidUpdate-currentState', 'yellow'], + ['componentDidUpdate-prevState', 'orange'], + ['setState-yellow', 'yellow'], + ['componentWillReceiveProps-start', 'yellow'], + // setState({color:'green'}) only enqueues a pending state. + ['componentWillReceiveProps-end', 'yellow'], + // pending state queue is processed + // before-setState-receiveProps never called, due to replaceState. + ['before-setState-again-receiveProps', undefined], + ['after-setState-receiveProps', 'green'], + ['shouldComponentUpdate-currentState', 'yellow'], + ['shouldComponentUpdate-nextState', 'green'], + ['componentWillUpdate-currentState', 'yellow'], + ['componentWillUpdate-nextState', 'green'], + ['render', 'green'], + ['componentDidUpdate-currentState', 'green'], + ['componentDidUpdate-prevState', 'yellow'], + ['setState-receiveProps', 'green'], + ['setProps', 'green'], + // setFavoriteColor('blue') + ['shouldComponentUpdate-currentState', 'green'], + ['shouldComponentUpdate-nextState', 'blue'], + ['componentWillUpdate-currentState', 'green'], + ['componentWillUpdate-nextState', 'blue'], + ['render', 'blue'], + ['componentDidUpdate-currentState', 'blue'], + ['componentDidUpdate-prevState', 'green'], + ['setFavoriteColor', 'blue'], + // forceUpdate() + ['componentWillUpdate-currentState', 'blue'], + ['componentWillUpdate-nextState', 'blue'], + ['render', 'blue'], + ['componentDidUpdate-currentState', 'blue'], + ['componentDidUpdate-prevState', 'blue'], + ['forceUpdate', 'blue'], + // unmountComponent() + // state is available within `componentWillUnmount()` + ['componentWillUnmount', 'blue'], + ]; + } else { + // There's a bug in the stack reconciler where setState callbacks inside + // componentWillMount aren't flushed properly + expected = [ + // there is no state when getInitialState() is called + ['getInitialState', null], + ['componentWillMount-start', 'red'], + // setState()'s only enqueue pending states. + ['componentWillMount-after-sunrise', 'red'], + ['componentWillMount-end', 'red'], + // pending state queue is processed + ['before-setState-sunrise', 'red'], + ['after-setState-sunrise', 'sunrise'], + ['after-setState-orange', 'orange'], + // pending state has been applied + ['render', 'orange'], + ['componentDidMount-start', 'orange'], + // setState-sunrise and setState-orange should be called here, + // after the bug in #1740 + // componentDidMount() called setState({color:'yellow'}), which is async. + // The update doesn't happen until the next flush. + ['componentDidMount-end', 'orange'], + ['shouldComponentUpdate-currentState', 'orange'], + ['shouldComponentUpdate-nextState', 'yellow'], + ['componentWillUpdate-currentState', 'orange'], + ['componentWillUpdate-nextState', 'yellow'], + ['render', 'yellow'], + ['componentDidUpdate-currentState', 'yellow'], + ['componentDidUpdate-prevState', 'orange'], + ['setState-sunrise', 'yellow'], + ['setState-orange', 'yellow'], + ['setState-yellow', 'yellow'], + ['initial-callback', 'yellow'], + ['componentWillReceiveProps-start', 'yellow'], + // setState({color:'green'}) only enqueues a pending state. + ['componentWillReceiveProps-end', 'yellow'], + // pending state queue is processed + // before-setState-receiveProps never called, due to replaceState. + ['before-setState-again-receiveProps', undefined], + ['after-setState-receiveProps', 'green'], + ['shouldComponentUpdate-currentState', 'yellow'], + ['shouldComponentUpdate-nextState', 'green'], + ['componentWillUpdate-currentState', 'yellow'], + ['componentWillUpdate-nextState', 'green'], + ['render', 'green'], + ['componentDidUpdate-currentState', 'green'], + ['componentDidUpdate-prevState', 'yellow'], + ['setState-receiveProps', 'green'], + ['setProps', 'green'], + // setFavoriteColor('blue') + ['shouldComponentUpdate-currentState', 'green'], + ['shouldComponentUpdate-nextState', 'blue'], + ['componentWillUpdate-currentState', 'green'], + ['componentWillUpdate-nextState', 'blue'], + ['render', 'blue'], + ['componentDidUpdate-currentState', 'blue'], + ['componentDidUpdate-prevState', 'green'], + ['setFavoriteColor', 'blue'], + // forceUpdate() + ['componentWillUpdate-currentState', 'blue'], + ['componentWillUpdate-nextState', 'blue'], + ['render', 'blue'], + ['componentDidUpdate-currentState', 'blue'], + ['componentDidUpdate-prevState', 'blue'], + ['forceUpdate', 'blue'], + // unmountComponent() + // state is available within `componentWillUnmount()` + ['componentWillUnmount', 'blue'], + ]; + } + + expect(stateListener.mock.calls.join('\n')).toEqual(expected.join('\n')); }); it('should batch unmounts', () => {