diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index d12b1aca9d392..8294883db55bc 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -131,6 +131,7 @@ var ReactDOM = { render(element : ReactElement, container : DOMContainerElement, callback: ?Function) { warnAboutUnstableUse(); let root; + if (!container._reactRootContainer) { root = container._reactRootContainer = DOMRenderer.mountContainer(element, container, callback); } else { diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 54ca2ef2cbb79..d861c81cd17df 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -12,6 +12,7 @@ 'use strict'; +import type { TrappedError } from 'ReactFiberErrorBoundary'; import type { Fiber } from 'ReactFiber'; import type { FiberRoot } from 'ReactFiberRoot'; import type { HostConfig } from 'ReactFiberReconciler'; @@ -205,8 +206,7 @@ module.exports = function(config : HostConfig) { // Now that the tree has been committed, we can handle errors. if (allTrappedErrors) { - // TODO: handle multiple errors with distinct boundaries. - handleError(allTrappedErrors[0]); + handleErrors(allTrappedErrors); } } @@ -377,7 +377,7 @@ module.exports = function(config : HostConfig) { throw error; } const trappedError = trapError(failedUnitOfWork, error); - handleError(trappedError); + handleErrors([trappedError]); } } @@ -408,6 +408,7 @@ module.exports = function(config : HostConfig) { // We're the only work scheduled. nextScheduledRoot = root; lastScheduledRoot = root; + scheduleDeferredCallback(performDeferredWork); } } @@ -443,7 +444,7 @@ module.exports = function(config : HostConfig) { throw error; } const trappedError = trapError(failedUnitOfWork, error); - handleError(trappedError); + handleErrors([trappedError]); } } @@ -471,56 +472,99 @@ module.exports = function(config : HostConfig) { } } - function handleError(trappedError) { - const boundary = trappedError.boundary; - const error = trappedError.error; - if (!boundary) { - throw error; + 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; + } - try { - // Give error boundary a chance to update its state - acknowledgeErrorInBoundary(boundary, error); - - // We will process an update caused by an error boundary with synchronous priority. - // This leaves us free to not keep track of whether a boundary has errored. - // If it errors again, we will just catch the error and synchronously propagate it higher. - - // First, traverse upwards and set pending synchronous priority on the whole tree. - let fiber = boundary; - while (fiber) { - fiber.pendingWorkPriority = SynchronousPriority; - if (fiber.alternate) { - fiber.alternate.pendingWorkPriority = SynchronousPriority; + function handleErrors(initialTrappedErrors : Array) : void { + let nextTrappedErrors = initialTrappedErrors; + let firstUncaughtError = 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. + while (nextTrappedErrors) { + const trappedErrors = nextTrappedErrors; + nextTrappedErrors = null; + + // Pass errors to all affected boundaries. + const affectedBoundaries : Set = new Set(); + trappedErrors.forEach(trappedError => { + const boundary = trappedError.boundary; + const error = trappedError.error; + if (!boundary) { + firstUncaughtError = firstUncaughtError || error; + return; } - if (!fiber.return) { - if (fiber.tag === HostContainer) { - // We found the root. - // Now go to the second phase and update it synchronously. - break; - } else { - throw new Error('Invalid root'); + // 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)); + } + }); + + // 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); + // 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) { + // If it throws, propagate the error. + nextTrappedErrors = nextTrappedErrors || []; + nextTrappedErrors.push(trapError(boundary, nextError)); } - fiber = fiber.return; - } - - if (!fiber) { - throw new Error('Could not find an error boundary root.'); - } + }); + } - // Find the work in progress tree. - const root : FiberRoot = (fiber.stateNode : any); - fiber = root.current.alternate; + // 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; - // Perform all the work synchronously. - while (fiber) { - fiber = performUnitOfWork(fiber, true); - } - } catch (nextError) { - // Propagate error to the next boundary or rethrow. - const nextTrappedError = trapError(boundary, nextError); - handleError(nextTrappedError); + // Throw any unhandled errors. + throw firstUncaughtError; } } diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js index 78fa5fe6ce8ca..405d6567c17ed 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js @@ -28,6 +28,7 @@ describe('ReactErrorBoundaries', () => { var BrokenComponentWillUnmount; var BrokenRenderErrorBoundary; var BrokenComponentWillMountErrorBoundary; + var BrokenComponentDidMountErrorBoundary; var BrokenRender; var ErrorBoundary; var ErrorMessage; @@ -35,9 +36,6 @@ describe('ReactErrorBoundaries', () => { var Normal; beforeEach(() => { - // TODO: Fiber isn't error resilient and one test can bring down them all. - jest.resetModuleRegistry(); - ReactDOM = require('ReactDOM'); React = require('React'); @@ -194,6 +192,9 @@ describe('ReactErrorBoundaries', () => { }; BrokenComponentDidUpdate = class extends React.Component { + static defaultProps = { + errorText: 'Hello', + }; constructor(props) { super(props); log.push('BrokenComponentDidUpdate constructor'); @@ -216,7 +217,7 @@ describe('ReactErrorBoundaries', () => { } componentDidUpdate() { log.push('BrokenComponentDidUpdate componentDidUpdate [!]'); - throw new Error('Hello'); + throw new Error(this.props.errorText); } componentWillUnmount() { log.push('BrokenComponentDidUpdate componentWillUnmount'); @@ -224,6 +225,9 @@ describe('ReactErrorBoundaries', () => { }; BrokenComponentWillUnmount = class extends React.Component { + static defaultProps = { + errorText: 'Hello', + }; constructor(props) { super(props); log.push('BrokenComponentWillUnmount constructor'); @@ -249,7 +253,7 @@ describe('ReactErrorBoundaries', () => { } componentWillUnmount() { log.push('BrokenComponentWillUnmount componentWillUnmount [!]'); - throw new Error('Hello'); + throw new Error(this.props.errorText); } }; @@ -283,6 +287,36 @@ describe('ReactErrorBoundaries', () => { } }; + BrokenComponentDidMountErrorBoundary = class extends React.Component { + constructor(props) { + super(props); + this.state = {error: null}; + log.push('BrokenComponentDidMountErrorBoundary constructor'); + } + render() { + if (this.state.error) { + log.push('BrokenComponentDidMountErrorBoundary render error'); + return
Caught an error: {this.state.error.message}.
; + } + log.push('BrokenComponentDidMountErrorBoundary render success'); + return
{this.props.children}
; + } + componentWillMount() { + log.push('BrokenComponentDidMountErrorBoundary componentWillMount'); + } + componentDidMount() { + log.push('BrokenComponentDidMountErrorBoundary componentDidMount [!]'); + throw new Error('Hello'); + } + componentWillUnmount() { + log.push('BrokenComponentDidMountErrorBoundary componentWillUnmount'); + } + unstable_handleError(error) { + log.push('BrokenComponentDidMountErrorBoundary unstable_handleError'); + this.setState({error}); + } + }; + BrokenRenderErrorBoundary = class extends React.Component { constructor(props) { super(props); @@ -397,43 +431,44 @@ describe('ReactErrorBoundaries', () => { }; ErrorBoundary = class extends React.Component { - constructor() { - super(); + constructor(props) { + super(props); this.state = {error: null}; - log.push('ErrorBoundary constructor'); + log.push(`${this.props.logName} constructor`); } render() { if (this.state.error && !this.props.forceRetry) { - log.push('ErrorBoundary render error'); + log.push(`${this.props.logName} render error`); return this.props.renderError(this.state.error, this.props); } - log.push('ErrorBoundary render success'); + log.push(`${this.props.logName} render success`); return
{this.props.children}
; } unstable_handleError(error) { - log.push('ErrorBoundary unstable_handleError'); + log.push(`${this.props.logName} unstable_handleError`); this.setState({error}); } componentWillMount() { - log.push('ErrorBoundary componentWillMount'); + log.push(`${this.props.logName} componentWillMount`); } componentDidMount() { - log.push('ErrorBoundary componentDidMount'); + log.push(`${this.props.logName} componentDidMount`); } componentWillReceiveProps() { - log.push('ErrorBoundary componentWillReceiveProps'); + log.push(`${this.props.logName} componentWillReceiveProps`); } componentWillUpdate() { - log.push('ErrorBoundary componentWillUpdate'); + log.push(`${this.props.logName} componentWillUpdate`); } componentDidUpdate() { - log.push('ErrorBoundary componentDidUpdate'); + log.push(`${this.props.logName} componentDidUpdate`); } componentWillUnmount() { - log.push('ErrorBoundary componentWillUnmount'); + log.push(`${this.props.logName} componentWillUnmount`); } }; ErrorBoundary.defaultProps = { + logName: 'ErrorBoundary', renderError(error, props) { return (
@@ -464,52 +499,89 @@ describe('ReactErrorBoundaries', () => { }; }); - if (ReactDOMFeatureFlags.useFiber) { - // This test implements a new feature in Fiber. - it('catches errors originating downstream', () => { - var fail = false; - class Stateful extends React.Component { - state = {shouldThrow: false}; + it('does not swallow exceptions on mounting without boundaries', () => { + var container = document.createElement('div'); + expect(() => { + ReactDOM.render(, container); + }).toThrow('Hello'); + + container = document.createElement('div'); + expect(() => { + ReactDOM.render(, container); + }).toThrow('Hello'); + + container = document.createElement('div'); + expect(() => { + ReactDOM.render(, container); + }).toThrow('Hello'); + }); - render() { - if (fail) { - log.push('Stateful render [!]'); - throw new Error('Hello'); - } - return
{this.props.children}
; - } - } + it('does not swallow exceptions on updating without boundaries', () => { + var container = document.createElement('div'); + ReactDOM.render(, container); + expect(() => { + ReactDOM.render(, container); + }).toThrow('Hello'); + + container = document.createElement('div'); + ReactDOM.render(, container); + expect(() => { + ReactDOM.render(, container); + }).toThrow('Hello'); + + container = document.createElement('div'); + ReactDOM.render(, container); + expect(() => { + ReactDOM.render(, container); + }).toThrow('Hello'); + }); - var statefulInst; - var container = document.createElement('div'); - ReactDOM.render( - - statefulInst = inst} /> - , - container - ); + it('does not swallow exceptions on unmounting without boundaries', () => { + var container = document.createElement('div'); + ReactDOM.render(, container); + expect(() => { + ReactDOM.unmountComponentAtNode(container); + }).toThrow('Hello'); + }); - log.length = 0; - expect(() => { - fail = true; - statefulInst.forceUpdate(); - }).not.toThrow(); + it('prevents errors from leaking into other roots', () => { + var container1 = document.createElement('div'); + var container2 = document.createElement('div'); + var container3 = document.createElement('div'); - expect(log).toEqual([ - 'Stateful render [!]', - 'ErrorBoundary unstable_handleError', - 'ErrorBoundary componentWillUpdate', - 'ErrorBoundary render error', - 'ErrorBoundary componentDidUpdate', - ]); + ReactDOM.render(Before 1, container1); + expect(() => { + ReactDOM.render(, container2); + }).toThrow('Hello'); + ReactDOM.render( + + + , + container3 + ); + expect(container1.firstChild.textContent).toBe('Before 1'); + expect(container2.firstChild).toBe(null); + expect(container3.firstChild.textContent).toBe('Caught an error: Hello.'); - log.length = 0; - ReactDOM.unmountComponentAtNode(container); - expect(log).toEqual([ - 'ErrorBoundary componentWillUnmount', - ]); - }); - } + ReactDOM.render(After 1, container1); + ReactDOM.render(After 2, container2); + ReactDOM.render( + + After 3 + , + container3 + ); + expect(container1.firstChild.textContent).toBe('After 1'); + expect(container2.firstChild.textContent).toBe('After 2'); + expect(container3.firstChild.textContent).toBe('After 3'); + + ReactDOM.unmountComponentAtNode(container1); + ReactDOM.unmountComponentAtNode(container2); + ReactDOM.unmountComponentAtNode(container3); + expect(container1.firstChild).toBe(null); + expect(container2.firstChild).toBe(null); + expect(container3.firstChild).toBe(null); + }); it('renders an error state if child throws in render', () => { var container = document.createElement('div'); @@ -661,65 +733,6 @@ describe('ReactErrorBoundaries', () => { ]); }); - if (ReactDOMFeatureFlags.useFiber) { - // This test implements a new feature in Fiber. - it('catches errors in componentDidMount', () => { - var container = document.createElement('div'); - ReactDOM.render( - - - - - - - , - container - ); - expect(log).toEqual([ - 'ErrorBoundary constructor', - 'ErrorBoundary componentWillMount', - 'ErrorBoundary render success', - 'BrokenComponentWillUnmount constructor', - 'BrokenComponentWillUnmount componentWillMount', - 'BrokenComponentWillUnmount render', - 'Normal constructor', - 'Normal componentWillMount', - 'Normal render', - 'BrokenComponentDidMount constructor', - 'BrokenComponentDidMount componentWillMount', - 'BrokenComponentDidMount render', - 'LastChild constructor', - 'LastChild componentWillMount', - 'LastChild render', - // Start flushing didMount queue - 'Normal componentDidMount', - 'BrokenComponentWillUnmount componentDidMount', - 'BrokenComponentDidMount componentDidMount [!]', - // Continue despite the error - 'LastChild componentDidMount', - 'ErrorBoundary componentDidMount', - // Now we are ready to handle the error - 'ErrorBoundary unstable_handleError', - 'ErrorBoundary componentWillUpdate', - 'ErrorBoundary render error', - // Safely unmount every child - 'BrokenComponentWillUnmount componentWillUnmount [!]', - // Continue unmounting safely despite any errors - 'Normal componentWillUnmount', - 'BrokenComponentDidMount componentWillUnmount', - 'LastChild componentWillUnmount', - // The update has finished - 'ErrorBoundary componentDidUpdate', - ]); - - log.length = 0; - ReactDOM.unmountComponentAtNode(container); - expect(log).toEqual([ - 'ErrorBoundary componentWillUnmount', - ]); - }); - } - it('propagates errors on retry on mounting', () => { var container = document.createElement('div'); ReactDOM.render( @@ -1356,50 +1369,6 @@ describe('ReactErrorBoundaries', () => { ]); }); - if (ReactDOMFeatureFlags.useFiber) { - // This test implements a new feature in Fiber. - it('catches errors in componentDidUpdate', () => { - var container = document.createElement('div'); - ReactDOM.render( - - - , - container - ); - - log.length = 0; - ReactDOM.render( - - - , - container - ); - expect(log).toEqual([ - 'ErrorBoundary componentWillReceiveProps', - 'ErrorBoundary componentWillUpdate', - 'ErrorBoundary render success', - 'BrokenComponentDidUpdate componentWillReceiveProps', - 'BrokenComponentDidUpdate componentWillUpdate', - 'BrokenComponentDidUpdate render', - // All lifecycles run - 'BrokenComponentDidUpdate componentDidUpdate [!]', - 'ErrorBoundary componentDidUpdate', - // Then, error is handled - 'ErrorBoundary unstable_handleError', - 'ErrorBoundary componentWillUpdate', - 'ErrorBoundary render error', - 'BrokenComponentDidUpdate componentWillUnmount', - 'ErrorBoundary componentDidUpdate', - ]); - - log.length = 0; - ReactDOM.unmountComponentAtNode(container); - expect(log).toEqual([ - 'ErrorBoundary componentWillUnmount', - ]); - }); - } - it('recovers from componentWillUnmount errors on update', () => { var container = document.createElement('div'); ReactDOM.render( @@ -1535,6 +1504,76 @@ describe('ReactErrorBoundaries', () => { ]); }); + it('picks the right boundary when handling unmounting errors', () => { + function renderInnerError(error) { + return
Caught an inner error: {error.message}.
; + } + function renderOuterError(error) { + return
Caught an outer error: {error.message}.
; + } + + var container = document.createElement('div'); + ReactDOM.render( + + + + + , + container + ); + + log.length = 0; + ReactDOM.render( + + + , + container + ); + expect(container.textContent).toBe('Caught an inner error: Hello.'); + expect(log).toEqual([ + // Update outer boundary + 'OuterErrorBoundary componentWillReceiveProps', + 'OuterErrorBoundary componentWillUpdate', + 'OuterErrorBoundary render success', + // Update inner boundary + 'InnerErrorBoundary componentWillReceiveProps', + 'InnerErrorBoundary componentWillUpdate', + 'InnerErrorBoundary render success', + // Try unmounting child + 'BrokenComponentWillUnmount componentWillUnmount [!]', + ...(ReactDOMFeatureFlags.useFiber ? [ + // Fiber proceeds with lifecycles despite errors + // Inner and outer boundaries have updated in this phase + 'InnerErrorBoundary componentDidUpdate', + 'OuterErrorBoundary componentDidUpdate', + // Now that commit phase is done, Fiber handles errors + // Only inner boundary receives the error: + 'InnerErrorBoundary unstable_handleError', + 'InnerErrorBoundary componentWillUpdate', + // Render an error now + 'InnerErrorBoundary render error', + // In Fiber, this was a local update to the + // inner boundary so only its hook fires + 'InnerErrorBoundary componentDidUpdate', + ] : [ + // Stack will handle error immediately + 'InnerErrorBoundary unstable_handleError', + 'InnerErrorBoundary render error', + // In stack, this was a part of the update to the + // outer boundary so both lifecycles fire + 'InnerErrorBoundary componentDidUpdate', + 'OuterErrorBoundary componentDidUpdate', + ]), + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'OuterErrorBoundary componentWillUnmount', + 'InnerErrorBoundary componentWillUnmount', + ]); + }); + it('can recover from error state', () => { var container = document.createElement('div'); ReactDOM.render( @@ -1701,4 +1740,284 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary componentWillUnmount', ]); }); + + // The tests below implement new features in Fiber. + if (ReactDOMFeatureFlags.useFiber) { + it('catches errors originating downstream', () => { + var fail = false; + class Stateful extends React.Component { + state = {shouldThrow: false}; + + render() { + if (fail) { + log.push('Stateful render [!]'); + throw new Error('Hello'); + } + return
{this.props.children}
; + } + } + + var statefulInst; + var container = document.createElement('div'); + ReactDOM.render( + + statefulInst = inst} /> + , + container + ); + + log.length = 0; + expect(() => { + fail = true; + statefulInst.forceUpdate(); + }).not.toThrow(); + + expect(log).toEqual([ + 'Stateful render [!]', + 'ErrorBoundary unstable_handleError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', + 'ErrorBoundary componentDidUpdate', + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'ErrorBoundary componentWillUnmount', + ]); + }); + + it('catches errors in componentDidMount', () => { + var container = document.createElement('div'); + ReactDOM.render( + + + + + + + , + container + ); + expect(log).toEqual([ + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenComponentWillUnmount constructor', + 'BrokenComponentWillUnmount componentWillMount', + 'BrokenComponentWillUnmount render', + 'Normal constructor', + 'Normal componentWillMount', + 'Normal render', + 'BrokenComponentDidMount constructor', + 'BrokenComponentDidMount componentWillMount', + 'BrokenComponentDidMount render', + 'LastChild constructor', + 'LastChild componentWillMount', + 'LastChild render', + // Start flushing didMount queue + 'Normal componentDidMount', + 'BrokenComponentWillUnmount componentDidMount', + 'BrokenComponentDidMount componentDidMount [!]', + // Continue despite the error + 'LastChild componentDidMount', + 'ErrorBoundary componentDidMount', + // Now we are ready to handle the error + 'ErrorBoundary unstable_handleError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', + // Safely unmount every child + 'BrokenComponentWillUnmount componentWillUnmount [!]', + // Continue unmounting safely despite any errors + 'Normal componentWillUnmount', + 'BrokenComponentDidMount componentWillUnmount', + 'LastChild componentWillUnmount', + // The update has finished + 'ErrorBoundary componentDidUpdate', + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'ErrorBoundary componentWillUnmount', + ]); + }); + + it('catches errors in componentDidUpdate', () => { + var container = document.createElement('div'); + ReactDOM.render( + + + , + container + ); + + log.length = 0; + ReactDOM.render( + + + , + container + ); + expect(log).toEqual([ + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render success', + 'BrokenComponentDidUpdate componentWillReceiveProps', + 'BrokenComponentDidUpdate componentWillUpdate', + 'BrokenComponentDidUpdate render', + // All lifecycles run + 'BrokenComponentDidUpdate componentDidUpdate [!]', + 'ErrorBoundary componentDidUpdate', + // Then, error is handled + 'ErrorBoundary unstable_handleError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', + 'BrokenComponentDidUpdate componentWillUnmount', + 'ErrorBoundary componentDidUpdate', + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'ErrorBoundary componentWillUnmount', + ]); + }); + + it('propagates errors inside boundary during componentDidMount', () => { + var container = document.createElement('div'); + ReactDOM.render( + + ( +
+ We should never catch our own error: {error.message}. +
+ )} /> +
, + container + ); + expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); + expect(log).toEqual([ + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenComponentDidMountErrorBoundary constructor', + 'BrokenComponentDidMountErrorBoundary componentWillMount', + 'BrokenComponentDidMountErrorBoundary render success', + 'BrokenComponentDidMountErrorBoundary componentDidMount [!]', + // Fiber proceeds with the hooks + 'ErrorBoundary componentDidMount', + // The error propagates to the higher boundary + 'ErrorBoundary unstable_handleError', + // Fiber retries from the root + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', + 'BrokenComponentDidMountErrorBoundary componentWillUnmount', + 'ErrorBoundary componentDidUpdate', + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'ErrorBoundary componentWillUnmount', + ]); + }); + + it('lets different boundaries catch their own first errors', () => { + function renderUnmountError(error) { + return
Caught an unmounting error: {error.message}.
; + } + function renderUpdateError(error) { + return
Caught an updating error: {error.message}.
; + } + + var container = document.createElement('div'); + ReactDOM.render( + + + + + + + + + + , + container + ); + + log.length = 0; + ReactDOM.render( + + + + + + + , + container + ); + + expect(container.firstChild.textContent).toBe( + 'Caught an unmounting error: E1.' + + 'Caught an updating error: E3.' + ); + expect(log).toEqual([ + // Begin update phase + 'OuterErrorBoundary componentWillReceiveProps', + 'OuterErrorBoundary componentWillUpdate', + 'OuterErrorBoundary render success', + 'InnerUnmountBoundary componentWillReceiveProps', + 'InnerUnmountBoundary componentWillUpdate', + 'InnerUnmountBoundary render success', + 'InnerUpdateBoundary componentWillReceiveProps', + 'InnerUpdateBoundary componentWillUpdate', + 'InnerUpdateBoundary render success', + // First come the updates + 'BrokenComponentDidUpdate componentWillReceiveProps', + 'BrokenComponentDidUpdate componentWillUpdate', + 'BrokenComponentDidUpdate render', + 'BrokenComponentDidUpdate componentWillReceiveProps', + 'BrokenComponentDidUpdate componentWillUpdate', + 'BrokenComponentDidUpdate render', + // We're in commit phase now, deleting + 'BrokenComponentWillUnmount componentWillUnmount [!]', + 'BrokenComponentWillUnmount componentWillUnmount [!]', + // Continue despite errors, handle them after commit is done + 'InnerUnmountBoundary componentDidUpdate', + // We're still in commit phase, now calling update lifecycles + 'BrokenComponentDidUpdate componentDidUpdate [!]', + // Again, continue despite errors, we'll handle them later + 'BrokenComponentDidUpdate componentDidUpdate [!]', + 'InnerUpdateBoundary componentDidUpdate', + 'OuterErrorBoundary componentDidUpdate', + // The interesting part starts now. + // Acknowledge errors independently but don't update yet: + 'InnerUnmountBoundary unstable_handleError', + 'InnerUpdateBoundary unstable_handleError', + // Only two of four errors are acknowledged: one per boundary. + // The rest are likely cascading and we ignore them. + // Now update: + 'InnerUnmountBoundary componentWillUpdate', + 'InnerUnmountBoundary render error', + 'InnerUpdateBoundary componentWillUpdate', + 'InnerUpdateBoundary render error', + // Commit + 'BrokenComponentDidUpdate componentWillUnmount', + 'BrokenComponentDidUpdate componentWillUnmount', + 'InnerUnmountBoundary componentDidUpdate', + 'InnerUpdateBoundary componentDidUpdate', + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'OuterErrorBoundary componentWillUnmount', + 'InnerUnmountBoundary componentWillUnmount', + 'InnerUpdateBoundary componentWillUnmount', + ]); + }); + } + });