diff --git a/scripts/fiber/tests-failing.txt b/scripts/fiber/tests-failing.txt index 955d305f69e0..c78722ebb6f1 100644 --- a/scripts/fiber/tests-failing.txt +++ b/scripts/fiber/tests-failing.txt @@ -554,9 +554,6 @@ src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponent-test.js src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentNestedState-test.js * should provide up to date values for props -src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentState-test.js -* should support setting state - src/renderers/shared/stack/reconciler/__tests__/ReactEmptyComponent-test.js * should still throw when rendering to undefined * should be able to switch between rendering null and a normal tag diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 42e9b95c1dfb..71f078f40969 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -791,6 +791,7 @@ src/renderers/shared/fiber/__tests__/ReactIncremental-test.js * skips will/DidUpdate when bailing unless an update was already in progress * performs batched updates at the end of the batch * can nest batchedUpdates +* propagates an error from a noop error boundary src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js * handles isMounted even when the initial render is deferred @@ -1016,6 +1017,7 @@ src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentDOMMinima * should not render extra nodes for non-interpolated text src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentState-test.js +* should support setting state * should batch unmounts src/renderers/shared/stack/reconciler/__tests__/ReactEmptyComponent-test.js diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index b23e5bdd6f8d..7a902e20977b 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -12,7 +12,6 @@ 'use strict'; -import type { TrappedError } from 'ReactFiberErrorBoundary'; import type { Fiber } from 'ReactFiber'; import type { FiberRoot } from 'ReactFiberRoot'; import type { HostConfig } from 'ReactFiberReconciler'; @@ -24,7 +23,6 @@ var { HostComponent, HostText, } = ReactTypeOfWork; -var { trapError } = require('ReactFiberErrorBoundary'); var { callCallbacks } = require('ReactFiberUpdateQueue'); var { @@ -33,7 +31,10 @@ var { Callback, } = require('ReactTypeOfSideEffect'); -module.exports = function(config : HostConfig) { +module.exports = function( + config : HostConfig, + trapError : (fiber : Fiber, error: Error, isUnmounting : boolean) => void +) { const updateContainer = config.updateContainer; const commitUpdate = config.commitUpdate; @@ -156,10 +157,7 @@ module.exports = function(config : HostConfig) { } } - function commitNestedUnmounts(root : Fiber): Array | null { - // Since errors are rare, we allocate this array on demand. - let trappedErrors = null; - + function commitNestedUnmounts(root : Fiber): void { // While we're inside a removed host node we don't want to call // removeChild on the inner nodes because they're removed by the top // call anyway. We also want to call componentWillUnmount on all @@ -167,58 +165,39 @@ module.exports = function(config : HostConfig) { // we do an inner loop while we're still inside the host node. let node : Fiber = root; while (true) { - const error = commitUnmount(node); - if (error) { - trappedErrors = trappedErrors || []; - trappedErrors.push(error); - } + commitUnmount(node); if (node.child) { // TODO: Coroutines need to visit the stateNode. node = node.child; continue; } if (node === root) { - return trappedErrors; + return; } while (!node.sibling) { if (!node.return || node.return === root) { - return trappedErrors; + return; } node = node.return; } node = node.sibling; } - return trappedErrors; } - function unmountHostComponents(parent, current): Array | null { - // Since errors are rare, we allocate this array on demand. - let trappedErrors = null; - + function unmountHostComponents(parent, current): void { // We only have the top Fiber that was inserted but we need recurse down its // children to find all the terminal nodes. let node : Fiber = current; while (true) { if (node.tag === HostComponent || node.tag === HostText) { - const errors = commitNestedUnmounts(node); - if (errors) { - if (!trappedErrors) { - trappedErrors = errors; - } else { - trappedErrors.push.apply(trappedErrors, errors); - } - } + commitNestedUnmounts(node); // After all the children have unmounted, it is now safe to remove the // node from the tree. if (parent) { removeChild(parent, node.stateNode); } } else { - const error = commitUnmount(node); - if (error) { - trappedErrors = trappedErrors || []; - trappedErrors.push(error); - } + commitUnmount(node); if (node.child) { // TODO: Coroutines need to visit the stateNode. node = node.child; @@ -226,24 +205,23 @@ module.exports = function(config : HostConfig) { } } if (node === current) { - return trappedErrors; + return; } while (!node.sibling) { if (!node.return || node.return === current) { - return trappedErrors; + return; } node = node.return; } node = node.sibling; } - return trappedErrors; } - function commitDeletion(current : Fiber) : Array | null { + function commitDeletion(current : Fiber) : void { // Recursively delete all host nodes from the parent. const parent = getHostParent(current); // Detach refs and call componentWillUnmount() on the whole subtree. - const trappedErrors = unmountHostComponents(parent, current); + unmountHostComponents(parent, current); // Cut off the return pointers to disconnect it from the tree. Ideally, we // should clear the child pointer of the parent alternate to let this @@ -256,11 +234,9 @@ module.exports = function(config : HostConfig) { current.alternate.child = null; current.alternate.return = null; } - - return trappedErrors; } - function commitUnmount(current : Fiber) : TrappedError | null { + function commitUnmount(current : Fiber) : void { switch (current.tag) { case ClassComponent: { detachRef(current); @@ -268,17 +244,14 @@ module.exports = function(config : HostConfig) { if (typeof instance.componentWillUnmount === 'function') { const error = tryCallComponentWillUnmount(instance); if (error) { - return trapError(current, error); + trapError(current, error, true); } } - return null; + return; } case HostComponent: { detachRef(current); - return null; - } - default: { - return null; + return; } } } @@ -323,7 +296,7 @@ module.exports = function(config : HostConfig) { } } - function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : TrappedError | null { + function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : void { switch (finishedWork.tag) { case ClassComponent: { const instance = finishedWork.stateNode; @@ -353,9 +326,9 @@ module.exports = function(config : HostConfig) { } } if (error) { - return trapError(finishedWork, error); + trapError(finishedWork, error, false); } - return null; + return; } case HostContainer: { const rootFiber = finishedWork.stateNode; @@ -364,15 +337,16 @@ module.exports = function(config : HostConfig) { rootFiber.callbackList = null; callCallbacks(callbackList, rootFiber.current.child.stateNode); } + return; } case HostComponent: { const instance : I = finishedWork.stateNode; attachRef(current, finishedWork, instance); - return null; + return; } case HostText: { // We have no life-cycles associated with text. - return null; + return; } default: throw new Error('This unit of work tag should not have side-effects.'); diff --git a/src/renderers/shared/fiber/ReactFiberErrorBoundary.js b/src/renderers/shared/fiber/ReactFiberErrorBoundary.js deleted file mode 100644 index 726888215596..000000000000 --- a/src/renderers/shared/fiber/ReactFiberErrorBoundary.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * 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. - * - * @providesModule ReactFiberErrorBoundary - * @flow - */ - -'use strict'; - -import type { Fiber } from 'ReactFiber'; - -var { - ClassComponent, -} = require('ReactTypeOfWork'); - -export type TrappedError = { - boundary: Fiber | null, - error: any, -}; - -function findClosestErrorBoundary(fiber : Fiber): Fiber | null { - let maybeErrorBoundary = fiber.return; - while (maybeErrorBoundary) { - if (maybeErrorBoundary.tag === ClassComponent) { - const instance = maybeErrorBoundary.stateNode; - if (typeof instance.unstable_handleError === 'function') { - return maybeErrorBoundary; - } - } - maybeErrorBoundary = maybeErrorBoundary.return; - } - return null; -} - -function trapError(fiber : Fiber, error : any) : TrappedError { - return { - boundary: findClosestErrorBoundary(fiber), - error, - }; -} - -function acknowledgeErrorInBoundary(boundary : Fiber, error : any) { - const instance = boundary.stateNode; - instance.unstable_handleError(error); -} - -exports.trapError = trapError; -exports.acknowledgeErrorInBoundary = acknowledgeErrorInBoundary; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 205b1ad04a55..dfd1fe4cdcb8 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -12,7 +12,6 @@ 'use strict'; -import type { TrappedError } from 'ReactFiberErrorBoundary'; import type { Fiber } from 'ReactFiber'; import type { FiberRoot } from 'ReactFiberRoot'; import type { HostConfig, Deadline } from 'ReactFiberReconciler'; @@ -24,13 +23,15 @@ var ReactFiberCommitWork = require('ReactFiberCommitWork'); var ReactCurrentOwner = require('ReactCurrentOwner'); var { cloneFiber } = require('ReactFiber'); -var { trapError, acknowledgeErrorInBoundary } = require('ReactFiberErrorBoundary'); var { NoWork, - LowPriority, - AnimationPriority, SynchronousPriority, + TaskPriority, + AnimationPriority, + HighPriority, + LowPriority, + OffscreenPriority, } = require('ReactPriorityLevel'); var { @@ -48,6 +49,7 @@ var { var { HostContainer, + ClassComponent, } = require('ReactTypeOfWork'); if (__DEV__) { @@ -56,14 +58,19 @@ if (__DEV__) { var timeHeuristicForUnitOfWork = 1; +type TrappedError = { + boundary: Fiber | null, + error: any, +}; + module.exports = function(config : HostConfig) { const { beginWork } = ReactFiberBeginWork(config, scheduleUpdate); const { completeWork } = ReactFiberCompleteWork(config); const { commitInsertion, commitDeletion, commitWork, commitLifeCycles } = - ReactFiberCommitWork(config); + ReactFiberCommitWork(config, trapError); - const scheduleAnimationCallback = config.scheduleAnimationCallback; - const scheduleDeferredCallback = config.scheduleDeferredCallback; + const hostScheduleAnimationCallback = config.scheduleAnimationCallback; + const hostScheduleDeferredCallback = config.scheduleDeferredCallback; const useSyncScheduling = config.useSyncScheduling; // The priority level to use when scheduling an update. @@ -74,6 +81,9 @@ module.exports = function(config : HostConfig) { // Whether updates should be batched. Only applies when using sync scheduling. let shouldBatchUpdates : boolean = false; + // Need this to prevent recursion while in a Task loop. + let isPerformingTaskWork : boolean = false; + // The next work in progress fiber that we're currently working on. let nextUnitOfWork : ?Fiber = null; let nextPriorityLevel : PriorityLevel = NoWork; @@ -86,6 +96,24 @@ module.exports = function(config : HostConfig) { let isAnimationCallbackScheduled : boolean = false; let isDeferredCallbackScheduled : boolean = false; + // Caught errors and error boundaries that are currently handling them + let activeErrorBoundaries : Set | null = null; + let nextTrappedErrors : Array | null = null; + + function scheduleAnimationCallback(callback) { + if (!isAnimationCallbackScheduled) { + isAnimationCallbackScheduled = true; + hostScheduleAnimationCallback(callback); + } + } + + function scheduleDeferredCallback(callback) { + if (!isDeferredCallbackScheduled) { + isDeferredCallbackScheduled = true; + hostScheduleDeferredCallback(callback); + } + } + function findNextUnitOfWork() { // Clear out roots with no more work on them. while (nextScheduledRoot && nextScheduledRoot.current.pendingWorkPriority === NoWork) { @@ -131,15 +159,8 @@ module.exports = function(config : HostConfig) { return null; } - function commitAllWork(finishedWork : Fiber, ignoreUnmountingErrors : boolean) { + function commitAllWork(finishedWork : Fiber) { // Commit all the side-effects within a tree. - - // Commit phase is meant to be atomic and non-interruptible. - // Any errors raised in it should be handled after it is over - // so that we don't end up in an inconsistent state due to user code. - // We'll keep track of all caught errors and handle them later. - let allTrappedErrors = null; - // First, we'll perform all the host insertions, updates, deletions and // ref unmounts. let effectfulFiber = finishedWork.firstEffect; @@ -150,7 +171,7 @@ module.exports = function(config : HostConfig) { commitInsertion(effectfulFiber); // Clear the "placement" from effect tag so that we know that this is inserted, before // any life-cycles like componentDidMount gets called. - effectfulFiber.effectTag = NoEffect; + effectfulFiber.effectTag ^= Placement; break; } case PlacementAndUpdate: @@ -159,7 +180,7 @@ module.exports = function(config : HostConfig) { commitInsertion(effectfulFiber); // Clear the "placement" from effect tag so that we know that this is inserted, before // any life-cycles like componentDidMount gets called. - effectfulFiber.effectTag = Update; + effectfulFiber.effectTag ^= Placement; // Update const current = effectfulFiber.alternate; @@ -173,20 +194,7 @@ module.exports = function(config : HostConfig) { break; case Deletion: case DeletionAndCallback: - // Deletion might cause an error in componentWillUnmount(). - // We will continue nevertheless and handle those later on. - const trappedErrors = commitDeletion(effectfulFiber); - // There is a special case where we completely ignore errors. - // It happens when we already caught an error earlier, and the update - // is caused by an error boundary trying to render an error message. - // In this case, we want to blow away the tree without catching errors. - if (trappedErrors && !ignoreUnmountingErrors) { - if (!allTrappedErrors) { - allTrappedErrors = trappedErrors; - } else { - allTrappedErrors.push.apply(allTrappedErrors, trappedErrors); - } - } + commitDeletion(effectfulFiber); break; } @@ -200,11 +208,7 @@ module.exports = function(config : HostConfig) { while (effectfulFiber) { if (effectfulFiber.effectTag & (Update | Callback)) { const current = effectfulFiber.alternate; - const trappedError = commitLifeCycles(current, effectfulFiber); - if (trappedError) { - allTrappedErrors = allTrappedErrors || []; - allTrappedErrors.push(trappedError); - } + commitLifeCycles(current, effectfulFiber); } const next = effectfulFiber.nextEffect; // Ensure that we clean these up so that we don't accidentally keep them. @@ -222,17 +226,11 @@ module.exports = function(config : HostConfig) { if (finishedWork.effectTag !== NoEffect) { const current = finishedWork.alternate; commitWork(current, finishedWork); - const trappedError = commitLifeCycles(current, finishedWork); - if (trappedError) { - allTrappedErrors = allTrappedErrors || []; - allTrappedErrors.push(trappedError); - } + commitLifeCycles(current, finishedWork); } - // Now that the tree has been committed, we can handle errors. - if (allTrappedErrors) { - handleErrors(allTrappedErrors); - } + // The task work includes batched updates and error handling. + performTaskWork(); } function resetWorkPriority(workInProgress : Fiber) { @@ -253,7 +251,7 @@ module.exports = function(config : HostConfig) { workInProgress.pendingWorkPriority = newPriority; } - function completeUnitOfWork(workInProgress : Fiber, ignoreUnmountingErrors : boolean) : ?Fiber { + function completeUnitOfWork(workInProgress : Fiber) : ?Fiber { while (true) { // The current, flushed, state of this fiber is the alternate. // Ideally nothing should rely on this, but relying on it here @@ -325,7 +323,7 @@ module.exports = function(config : HostConfig) { // "next" scheduled work since we've already scanned passed. That // also ensures that work scheduled during reconciliation gets deferred. // const hasMoreWork = workInProgress.pendingWorkPriority !== NoWork; - commitAllWork(workInProgress, ignoreUnmountingErrors); + commitAllWork(workInProgress); const nextWork = findNextUnitOfWork(); // if (!nextWork && hasMoreWork) { // TODO: This can happen when some deep work completes and we don't @@ -339,7 +337,7 @@ module.exports = function(config : HostConfig) { } } - function performUnitOfWork(workInProgress : Fiber, ignoreUnmountingErrors : boolean) : ?Fiber { + function performUnitOfWork(workInProgress : Fiber) : ?Fiber { // The current, flushed, state of this fiber is the alternate. // Ideally nothing should rely on this, but relying on it here // means that we don't need an additional field on the work in @@ -360,7 +358,7 @@ module.exports = function(config : HostConfig) { ReactFiberInstrumentation.debugTool.onWillCompleteWork(workInProgress); } // If this doesn't spawn new work, complete the current work. - next = completeUnitOfWork(workInProgress, ignoreUnmountingErrors); + next = completeUnitOfWork(workInProgress); if (__DEV__ && ReactFiberInstrumentation.debugTool) { ReactFiberInstrumentation.debugTool.onDidCompleteWork(workInProgress); } @@ -377,16 +375,13 @@ module.exports = function(config : HostConfig) { } while (nextUnitOfWork) { if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false); + nextUnitOfWork = performUnitOfWork(nextUnitOfWork); if (!nextUnitOfWork) { // Find more work. We might have time to complete some more. nextUnitOfWork = findNextUnitOfWork(); } } else { - if (!isDeferredCallbackScheduled) { - isDeferredCallbackScheduled = true; - scheduleDeferredCallback(performDeferredWork); - } + scheduleDeferredCallback(performDeferredWork); return; } } @@ -397,56 +392,19 @@ module.exports = function(config : HostConfig) { performAndHandleErrors(LowPriority, deadline); } - function scheduleDeferredWork(root : FiberRoot, priority : PriorityLevel) { - // We must reset the current unit of work pointer so that we restart the - // search from the root during the next tick, in case there is now higher - // priority work somewhere earlier than before. - if (priority <= nextPriorityLevel) { - nextUnitOfWork = null; - } - - // Set the priority on the root, without deprioritizing - if (root.current.pendingWorkPriority === NoWork || - priority <= root.current.pendingWorkPriority) { - root.current.pendingWorkPriority = priority; - } - - if (!root.isScheduled) { - root.isScheduled = true; - if (lastScheduledRoot) { - // Schedule ourselves to the end. - lastScheduledRoot.nextScheduledRoot = root; - lastScheduledRoot = root; - } else { - // We're the only work scheduled. - nextScheduledRoot = root; - lastScheduledRoot = root; - } - } - - if (!isDeferredCallbackScheduled) { - isDeferredCallbackScheduled = true; - scheduleDeferredCallback(performDeferredWork); - } - } - function performAnimationWorkUnsafe() { // Always start from the root nextUnitOfWork = findNextUnitOfWork(); while (nextUnitOfWork && - nextPriorityLevel !== NoWork && - nextPriorityLevel <= AnimationPriority) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false); + nextPriorityLevel === AnimationPriority) { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork); if (!nextUnitOfWork) { // Keep searching for animation work until there's no more left nextUnitOfWork = findNextUnitOfWork(); } } - if (nextUnitOfWork && nextPriorityLevel > AnimationPriority) { - if (!isDeferredCallbackScheduled) { - isDeferredCallbackScheduled = true; - scheduleDeferredCallback(performDeferredWork); - } + if (nextUnitOfWork) { + scheduleCallbackAtPriority(nextPriorityLevel); } } @@ -455,81 +413,18 @@ module.exports = function(config : HostConfig) { performAndHandleErrors(AnimationPriority); } - function scheduleAnimationWork(root: FiberRoot, priorityLevel : PriorityLevel) { - // Set the priority on the root, without deprioritizing - if (root.current.pendingWorkPriority === NoWork || - priorityLevel <= root.current.pendingWorkPriority) { - root.current.pendingWorkPriority = priorityLevel; - } - - if (!root.isScheduled) { - root.isScheduled = true; - if (lastScheduledRoot) { - // Schedule ourselves to the end. - lastScheduledRoot.nextScheduledRoot = root; - lastScheduledRoot = root; - } else { - // We're the only work scheduled. - nextScheduledRoot = root; - lastScheduledRoot = root; - } - } - - if (!isAnimationCallbackScheduled) { - isAnimationCallbackScheduled = true; - scheduleAnimationCallback(performAnimationWork); - } - } - - function scheduleErrorBoundaryWork(boundary : Fiber, priority) : FiberRoot { - let root = null; - 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; - } else { - throw new Error('Invalid root'); - } - } - fiber = fiber.return; - } - if (!root) { - throw new Error('Could not find root from the boundary.'); - } - return root; - } - function performSynchronousWorkUnsafe() { - // Perform work now nextUnitOfWork = findNextUnitOfWork(); while (nextUnitOfWork && nextPriorityLevel === SynchronousPriority) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false); + nextUnitOfWork = performUnitOfWork(nextUnitOfWork); if (!nextUnitOfWork) { nextUnitOfWork = findNextUnitOfWork(); } } if (nextUnitOfWork) { - if (nextPriorityLevel > AnimationPriority) { - if (!isDeferredCallbackScheduled) { - isDeferredCallbackScheduled = true; - scheduleDeferredCallback(performDeferredWork); - } - return; - } - if (!isAnimationCallbackScheduled) { - isAnimationCallbackScheduled = true; - scheduleAnimationCallback(performAnimationWork); - } + scheduleCallbackAtPriority(nextPriorityLevel); } } @@ -544,148 +439,255 @@ module.exports = function(config : HostConfig) { } } - function scheduleSynchronousWork(root : FiberRoot) { - root.current.pendingWorkPriority = SynchronousPriority; - - if (root.isScheduled) { - // If we're already scheduled, we can bail out. - return; + function performTaskWorkUnsafe() { + if (isPerformingTaskWork) { + throw new Error('Already performing task work'); } - 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(); + isPerformingTaskWork = true; + nextUnitOfWork = findNextUnitOfWork(); + while (nextUnitOfWork && + nextPriorityLevel === TaskPriority) { + nextUnitOfWork = + performUnitOfWork(nextUnitOfWork); + + if (!nextUnitOfWork) { + nextUnitOfWork = findNextUnitOfWork(); } } + if (nextUnitOfWork) { + scheduleCallbackAtPriority(nextPriorityLevel); + } + isPerformingTaskWork = false; + } + + function performTaskWork() { + performAndHandleErrors(TaskPriority); } function performAndHandleErrors(priorityLevel : PriorityLevel, deadline : null | 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 { - performDeferredWorkUnsafe(deadline); - } - return; - } else { - performAnimationWorkUnsafe(); + switch (priorityLevel) { + case SynchronousPriority: + performSynchronousWorkUnsafe(); + break; + case TaskPriority: + if (!isPerformingTaskWork) { + performTaskWorkUnsafe(); + } + break; + case AnimationPriority: + performAnimationWorkUnsafe(); + break; + case HighPriority: + case LowPriority: + case OffscreenPriority: + if (!deadline) { + throw new Error('No deadline'); + } else { + performDeferredWorkUnsafe(deadline); + } + break; + default: + break; } } 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]); + trapError(nextUnitOfWork, error, false); + } + + // If there were errors and we aren't already handling them, handle them now + if (nextTrappedErrors && !activeErrorBoundaries) { + handleErrors(); } } - function handleErrors(initialTrappedErrors : Array) : void { - let nextTrappedErrors = initialTrappedErrors; - let firstUncaughtError = null; + function handleErrors() : void { + if (activeErrorBoundaries) { + throw new Error('Already handling errors'); + } - // 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. - while (nextTrappedErrors) { - const trappedErrors = nextTrappedErrors; - nextTrappedErrors = null; + // Start tracking active boundaries. + activeErrorBoundaries = new Set(); - // Pass errors to all affected boundaries. - const affectedBoundaries : Set = new Set(); - trappedErrors.forEach(trappedError => { + // If we find unhandled errors, we'll only rethrow the first one. + let firstUncaughtError = null; + + // All work created by error boundaries should have Task priority + // so that it finishes before this function exits. + const previousPriorityContext = priorityContext; + priorityContext = TaskPriority; + + // Keep looping until there are no more trapped errors, or until we find + // an unhandled error. + while (nextTrappedErrors && nextTrappedErrors.length && !firstUncaughtError) { + // First, find all error boundaries and notify them about errors. + while (nextTrappedErrors && nextTrappedErrors.length) { + const trappedError = nextTrappedErrors.shift(); const boundary = trappedError.boundary; const error = trappedError.error; if (!boundary) { firstUncaughtError = firstUncaughtError || error; - return; + continue; } // Don't visit boundaries twice. - if (affectedBoundaries.has(boundary)) { - return; - } - // Give error boundary a chance to update its state. - try { - acknowledgeErrorInBoundary(boundary, error); - affectedBoundaries.add(boundary); - } catch (nextError) { - // If it throws, propagate the error. - nextTrappedErrors = nextTrappedErrors || []; - nextTrappedErrors.push(trapError(boundary, nextError)); + if (activeErrorBoundaries.has(boundary)) { + continue; } - }); - - // We will process an update caused by each error boundary synchronously. - affectedBoundaries.forEach(boundary => { - const priority = priorityContext; - const root = 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); - } + // This error boundary is now active. + // We will let it handle the error and retry rendering. + // If it fails again, the next error will be propagated to the parent + // boundary or rethrown. + activeErrorBoundaries.add(boundary); + // Give error boundary a chance to update its state. + // Updates will be scheduled with Task priority. + const instance = boundary.stateNode; + instance.unstable_handleError(error); + + // Schedule an update, in case the boundary didn't call setState + // on itself. + scheduleUpdate(boundary); } catch (nextError) { - // If it throws, propagate the error. - nextTrappedErrors = nextTrappedErrors || []; - nextTrappedErrors.push(trapError(boundary, nextError)); + // If an error is thrown, propagate the error to the parent boundary. + trapError(boundary, nextError, false); } - }); + } + + // Now that we attempt to flush any work that was scheduled by the boundaries. + // If this creates errors, they will be pushed to nextTrappedErrors and + // the outer loop will continue. + try { + performTaskWorkUnsafe(); + } catch (error) { + isPerformingTaskWork = false; + trapError(nextUnitOfWork, error, false); + } } - ReactCurrentOwner.current = null; + nextTrappedErrors = null; + activeErrorBoundaries = null; + priorityContext = previousPriorityContext; - // 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 findClosestErrorBoundary(fiber : Fiber): Fiber | null { + let maybeErrorBoundary = fiber.return; + while (maybeErrorBoundary) { + if (maybeErrorBoundary.tag === ClassComponent) { + const instance = maybeErrorBoundary.stateNode; + const isErrorBoundary = typeof instance.unstable_handleError === 'function'; + if (isErrorBoundary) { + const isHandlingAnotherError = ( + activeErrorBoundaries !== null && + activeErrorBoundaries.has(maybeErrorBoundary) + ); + if (!isHandlingAnotherError) { + return maybeErrorBoundary; + } + } + } + maybeErrorBoundary = maybeErrorBoundary.return; + } + return null; + } + + function trapError(fiber : Fiber | null, error : any, isUnmounting : boolean) : void { + // It is no longer valid because we exited the user code. + ReactCurrentOwner.current = null; + + if (isUnmounting && activeErrorBoundaries) { + // Ignore errors caused by unmounting during error handling. + // This lets error boundaries safely tear down already failed trees. + return; + } + + if (!nextTrappedErrors) { + nextTrappedErrors = []; + } + nextTrappedErrors.push({ + boundary: fiber ? findClosestErrorBoundary(fiber) : null, + error, + }); + } + function scheduleWork(root : FiberRoot) { - scheduleWorkAtPriority(root, priorityContext); + let priorityLevel = priorityContext; + + // If we're in a batch, switch to task priority + if (priorityLevel === SynchronousPriority && shouldBatchUpdates) { + priorityLevel = TaskPriority; + } + + scheduleWorkAtPriority(root, priorityLevel); } function scheduleWorkAtPriority(root : FiberRoot, priorityLevel : PriorityLevel) { - if (priorityLevel === NoWork) { - return; - } else if (priorityLevel === SynchronousPriority) { - scheduleSynchronousWork(root); - } else if (priorityLevel <= AnimationPriority) { - scheduleAnimationWork(root, priorityLevel); - } else { - scheduleDeferredWork(root, priorityLevel); - return; + // Set the priority on the root, without deprioritizing + if (root.current.pendingWorkPriority === NoWork || + priorityLevel <= root.current.pendingWorkPriority) { + root.current.pendingWorkPriority = priorityLevel; + } + + if (!root.isScheduled) { + root.isScheduled = true; + if (lastScheduledRoot) { + // Schedule ourselves to the end. + lastScheduledRoot.nextScheduledRoot = root; + lastScheduledRoot = root; + } else { + // We're the only work scheduled. + nextScheduledRoot = root; + lastScheduledRoot = root; + } + } + + // We must reset the current unit of work pointer so that we restart the + // search from the root during the next tick, in case there is now higher + // priority work somewhere earlier than before. + if (priorityLevel <= nextPriorityLevel) { + nextUnitOfWork = null; + } + + scheduleCallbackAtPriority(priorityLevel); + } + + function scheduleCallbackAtPriority(priorityLevel : PriorityLevel) { + switch (priorityLevel) { + case SynchronousPriority: + // Perform work immediately + performSynchronousWork(); + return; + case TaskPriority: + // Do nothing. Task work should be flushed after committing. + return; + case AnimationPriority: + scheduleAnimationCallback(performAnimationWork); + return; + case HighPriority: + case LowPriority: + case OffscreenPriority: + scheduleDeferredCallback(performDeferredWork); + return; } } function scheduleUpdate(fiber : Fiber) { - const priorityLevel = priorityContext; + let priorityLevel = priorityContext; + // If we're in a batch, switch to task priority + if (priorityLevel === SynchronousPriority && shouldBatchUpdates) { + priorityLevel = TaskPriority; + } + while (true) { if (fiber.pendingWorkPriority === NoWork || fiber.pendingWorkPriority >= priorityLevel) { @@ -727,9 +729,9 @@ module.exports = function(config : HostConfig) { return fn(); } finally { shouldBatchUpdates = prev; - // If we've exited the batch, perform any scheduled sync work + // If we've exited the batch, perform any scheduled task work if (!shouldBatchUpdates) { - performSynchronousWork(); + performTaskWork(); } } } @@ -746,7 +748,6 @@ module.exports = function(config : HostConfig) { return { scheduleWork: scheduleWork, - scheduleDeferredWork: scheduleDeferredWork, performWithPriority: performWithPriority, batchedUpdates: batchedUpdates, syncUpdates: syncUpdates, diff --git a/src/renderers/shared/fiber/ReactPriorityLevel.js b/src/renderers/shared/fiber/ReactPriorityLevel.js index b6930c2900a0..a0c9a7a7c07c 100644 --- a/src/renderers/shared/fiber/ReactPriorityLevel.js +++ b/src/renderers/shared/fiber/ReactPriorityLevel.js @@ -12,13 +12,14 @@ 'use strict'; -export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5; +export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6; module.exports = { NoWork: 0, // No work is pending. SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects. - AnimationPriority: 2, // Needs to complete before the next frame. - HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive. - LowPriority: 4, // Data fetching, or result from updating stores. - OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible. + TaskPriority: 2, // Completes at the end of the current tick. + AnimationPriority: 3, // Needs to complete before the next frame. + HighPriority: 4, // Interaction that needs to complete pretty soon to feel responsive. + LowPriority: 5, // Data fetching, or result from updating stores. + OffscreenPriority: 6, // Won't be visible but do the work in case it becomes visible. }; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index 5df5c10caf80..8fa9afe489d0 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -1465,4 +1465,27 @@ describe('ReactIncremental', () => { ]); expect(instance.state.n).toEqual(4); }); + + it('propagates an error from a noop error boundary', () => { + class NoopBoundary extends React.Component { + unstable_handleError() { + // Noop + } + render() { + return this.props.children; + } + } + + function RenderError() { + throw new Error('render error'); + } + + ReactNoop.render( + + + + ); + + expect(ReactNoop.flush).toThrow('render error'); + }); });