diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index a1fc0b9986742..288e8aa9b401b 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -175,7 +175,12 @@ module.exports = function(config : HostConfig, s var ctor = workInProgress.type; workInProgress.stateNode = instance = new ctor(props); mount(workInProgress, instance); - state = instance.state || null; + updateQueue = workInProgress.updateQueue; + if (updateQueue) { + state = mergeUpdateQueue(updateQueue, instance.state, props); + } else { + state = null; + } } else if (typeof instance.shouldComponentUpdate === 'function' && !(updateQueue && updateQueue.isForced)) { if (workInProgress.memoizedProps !== null) { diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 6110057607cb7..f2f873a82ad95 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -80,6 +80,10 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori // The instance needs access to the fiber so that it can schedule updates ReactInstanceMap.set(instance, workInProgress); instance.updater = updater; + + if (typeof instance.componentWillMount === 'function') { + instance.componentWillMount(); + } } return { diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 99c8682a17ebe..8eb2fff78c128 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -40,6 +40,7 @@ var { var { HostContainer, + ClassComponent, } = require('ReactTypeOfWork'); var timeHeuristicForUnitOfWork = 1; @@ -285,7 +286,7 @@ module.exports = function(config : HostConfig) { } } - function performDeferredWork(deadline) { + function performDeferredWorkUnsafe(deadline) { if (!nextUnitOfWork) { nextUnitOfWork = findNextUnitOfWork(); } @@ -303,6 +304,23 @@ 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) { + handleError(failedUnitOfWork, error); + } else { + // We shouldn't end up here because nextUnitOfWork + // should always be set while work is being performed. + throw error; + } + } + } + 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 @@ -334,7 +352,7 @@ module.exports = function(config : HostConfig) { } } - function performAnimationWork() { + function performAnimationWorkUnsafe() { // Always start from the root nextUnitOfWork = findNextUnitOfWork(); while (nextUnitOfWork && @@ -352,6 +370,23 @@ module.exports = function(config : HostConfig) { } } + function performAnimationWork() { + try { + performAnimationWorkUnsafe(); + } catch (error) { + const failedUnitOfWork = nextUnitOfWork; + // Reset because it points to the error boundary: + nextUnitOfWork = null; + if (failedUnitOfWork) { + handleError(failedUnitOfWork, error); + } else { + // We shouldn't end up here because nextUnitOfWork + // should always be set while work is being performed. + throw error; + } + } + } + function scheduleAnimationWork(root: FiberRoot, priorityLevel : PriorityLevel) { // Set the priority on the root, without deprioritizing if (root.current.pendingWorkPriority === NoWork || @@ -426,6 +461,60 @@ module.exports = function(config : HostConfig) { } } + function findClosestErrorBoundary(fiber : Fiber): ?Fiber { + 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 handleError(failedUnitOfWork : Fiber, error : any) { + const errorBoundary = findClosestErrorBoundary(failedUnitOfWork); + if (errorBoundary) { + handleErrorInBoundary(errorBoundary, error); + return; + } + // TODO: Do we need to reset nextUnitOfWork here? + throw error; + } + + function handleErrorInBoundary(errorBoundary : Fiber, error : any) { + // The work below failed so we need to clear it out and try to render the error state. + // TODO: Do we need to clear all of these fields? How do we teardown an existing tree? + errorBoundary.child = null; + errorBoundary.effectTag = NoEffect; + errorBoundary.nextEffect = null; + errorBoundary.firstEffect = null; + errorBoundary.lastEffect = null; + errorBoundary.progressedPriority = NoWork; + errorBoundary.progressedChild = null; + errorBoundary.progressedFirstDeletion = null; + errorBoundary.progressedLastDeletion = null; + + // Error boundary implementations would usually call setState() here: + const instance = errorBoundary.stateNode; + instance.unstable_handleError(error); + + try { + // The error path should be processed synchronously. + // This lets us easily propagate errors to a parent boundary. + let unitOfWork = errorBoundary; + while (unitOfWork) { + unitOfWork = performUnitOfWork(unitOfWork); + } + } catch (nextError) { + // Propagate error to the next boundary or rethrow. + handleError(errorBoundary, nextError); + } + } + return { scheduleWork: scheduleWork, scheduleDeferredWork: scheduleDeferredWork, diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js index ac5e410cad911..7ec6cb14d2875 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js @@ -11,6 +11,8 @@ 'use strict'; +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); + var React; var ReactDOM; @@ -33,6 +35,9 @@ 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'); @@ -459,52 +464,93 @@ describe('ReactErrorBoundaries', () => { }; }); - // Known limitation: error boundary only "sees" errors caused by updates - // flowing through it. This might be easier to fix in Fiber. - it('currently does not catch errors originating downstream', () => { - var fail = false; - class Stateful extends React.Component { - state = {shouldThrow: false}; + 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}; + + render() { + if (fail) { + log.push('Stateful render [!]'); + throw new Error('Hello'); + } + return
; + } + } - render() { - if (fail) { - log.push('Stateful render [!]'); - throw new Error('Hello'); + 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 render error', + 'ErrorBoundary componentDidUpdate', + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'ErrorBoundary componentWillUnmount', + ]); + }); + } else { + // Known limitation: error boundary only "sees" errors caused by updates + // flowing through it. This is fixed in Fiber. + it('currently does not catch 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
; } - return
; } - } - var statefulInst; - var container = document.createElement('div'); - ReactDOM.render( - - statefulInst = inst} /> - , - container - ); + var statefulInst; + var container = document.createElement('div'); + ReactDOM.render( + + statefulInst = inst} /> + , + container + ); - log.length = 0; - expect(() => { - fail = true; - statefulInst.forceUpdate(); - }).toThrow(); + log.length = 0; + expect(() => { + fail = true; + statefulInst.forceUpdate(); + }).toThrow(); - expect(log).toEqual([ - 'Stateful render [!]', - // FIXME: uncomment when downstream errors get caught. - // Catch and render an error message - // 'ErrorBoundary unstable_handleError', - // 'ErrorBoundary render error', - // 'ErrorBoundary componentDidUpdate', - ]); + expect(log).toEqual([ + 'Stateful render [!]', + ]); - log.length = 0; - ReactDOM.unmountComponentAtNode(container); - expect(log).toEqual([ - 'ErrorBoundary componentWillUnmount', - ]); - }); + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'ErrorBoundary componentWillUnmount', + ]); + }); + } it('renders an error state if child throws in render', () => { var container = document.createElement('div'); @@ -860,7 +906,7 @@ describe('ReactErrorBoundaries', () => { 'BrokenRender render [!]', // Handle error: 'ErrorBoundary unstable_handleError', - // Child ref wasn't (and won't be) set but there's no harm in clearing: + // TODO: This is unnecessary, and Fiber doesn't do it: 'Child ref is set to null', 'ErrorBoundary render error', // Ref to error message should get set: