diff --git a/src/renderers/dom/__tests__/ReactDOMProduction-test.js b/src/renderers/dom/__tests__/ReactDOMProduction-test.js index a50e7eedc01af..5b1d4ebf94835 100644 --- a/src/renderers/dom/__tests__/ReactDOMProduction-test.js +++ b/src/renderers/dom/__tests__/ReactDOMProduction-test.js @@ -86,6 +86,94 @@ describe('ReactDOMProduction', function() { expect(container.childNodes.length).toBe(0); }); + it('should call lifecycle methods', function() { + var log = []; + class Component extends React.Component { + state = {y: 1}; + shouldComponentUpdate(nextProps, nextState) { + log.push(['shouldComponentUpdate', nextProps, nextState]); + return nextProps.x !== this.props.x || nextState.y !== this.state.y; + } + componentWillMount() { + log.push(['componentWillMount']); + } + componentDidMount() { + log.push(['componentDidMount']); + } + componentWillReceiveProps(nextProps) { + log.push(['componentWillReceiveProps', nextProps]); + } + componentWillUpdate(nextProps, nextState) { + log.push(['componentWillUpdate', nextProps, nextState]); + } + componentDidUpdate(prevProps, prevState) { + log.push(['componentDidUpdate', prevProps, prevState]); + } + componentWillUnmount() { + log.push(['componentWillUnmount']); + } + render() { + log.push(['render']); + return null; + } + } + + var container = document.createElement('div'); + var inst = ReactDOM.render( + , + container + ); + expect(log).toEqual([ + ['componentWillMount'], + ['render'], + ['componentDidMount'], + ]); + log = []; + + inst.setState({y: 2}); + expect(log).toEqual([ + ['shouldComponentUpdate', {x: 1}, {y: 2}], + ['componentWillUpdate', {x: 1}, {y: 2}], + ['render'], + ['componentDidUpdate', {x: 1}, {y: 1}], + ]); + log = []; + + inst.setState({y: 2}); + expect(log).toEqual([ + ['shouldComponentUpdate', {x: 1}, {y: 2}], + ]); + log = []; + + ReactDOM.render( + , + container + ); + expect(log).toEqual([ + ['componentWillReceiveProps', {x: 2}], + ['shouldComponentUpdate', {x: 2}, {y: 2}], + ['componentWillUpdate', {x: 2}, {y: 2}], + ['render'], + ['componentDidUpdate', {x: 1}, {y: 2}], + ]); + log = []; + + ReactDOM.render( + , + container + ); + expect(log).toEqual([ + ['componentWillReceiveProps', {x: 2}], + ['shouldComponentUpdate', {x: 2}, {y: 2}], + ]); + log = []; + + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + ['componentWillUnmount'], + ]); + }); + it('should throw with an error code in production', function() { expect(function() { class Component extends React.Component { diff --git a/src/renderers/shared/ReactDebugTool.js b/src/renderers/shared/ReactDebugTool.js index e976445c3d635..a59053d3a35d7 100644 --- a/src/renderers/shared/ReactDebugTool.js +++ b/src/renderers/shared/ReactDebugTool.js @@ -251,12 +251,6 @@ var ReactDebugTool = { endLifeCycleTimer(debugID, timerType); emitEvent('onEndLifeCycleTimer', debugID, timerType); }, - onError(debugID) { - if (currentTimerDebugID != null) { - endLifeCycleTimer(currentTimerDebugID, currentTimerType); - } - emitEvent('onError', debugID); - }, onBeginProcessingChildContext() { emitEvent('onBeginProcessingChildContext'); }, diff --git a/src/renderers/shared/__tests__/ReactPerf-test.js b/src/renderers/shared/__tests__/ReactPerf-test.js index 99e5c9494312e..70e5cd5f53fa3 100644 --- a/src/renderers/shared/__tests__/ReactPerf-test.js +++ b/src/renderers/shared/__tests__/ReactPerf-test.js @@ -516,4 +516,208 @@ describe('ReactPerf', function() { renderCount: 2, }]); }); + + it('should not print errant warnings if render() throws', () => { + var container = document.createElement('div'); + var thrownErr = new Error('Muhaha!'); + + class Evil extends React.Component { + render() { + throw thrownErr; + } + } + + ReactPerf.start(); + try { + ReactDOM.render( +
+ + +
, + container + ); + } catch (err) { + if (err !== thrownErr) { + throw err; + } + } + ReactPerf.stop(); + }); + + it('should not print errant warnings if componentWillMount() throws', () => { + var container = document.createElement('div'); + var thrownErr = new Error('Muhaha!'); + + class Evil extends React.Component { + componentWillMount() { + throw thrownErr; + } + render() { + return
; + } + } + + ReactPerf.start(); + try { + ReactDOM.render( +
+ + +
, + container + ); + } catch (err) { + if (err !== thrownErr) { + throw err; + } + } + ReactPerf.stop(); + }); + + it('should not print errant warnings if componentDidMount() throws', () => { + var container = document.createElement('div'); + var thrownErr = new Error('Muhaha!'); + + class Evil extends React.Component { + componentDidMount() { + throw thrownErr; + } + render() { + return
; + } + } + + ReactPerf.start(); + try { + ReactDOM.render( +
+ + +
, + container + ); + } catch (err) { + if (err !== thrownErr) { + throw err; + } + } + ReactPerf.stop(); + }); + + it('should not print errant warnings if portal throws in render()', () => { + var container = document.createElement('div'); + var thrownErr = new Error('Muhaha!'); + + class Evil extends React.Component { + render() { + throw thrownErr; + } + } + class EvilPortal extends React.Component { + componentWillMount() { + var portalContainer = document.createElement('div'); + ReactDOM.render(, portalContainer); + } + render() { + return
; + } + } + + ReactPerf.start(); + try { + ReactDOM.render( +
+ + +
, + container + ); + } catch (err) { + if (err !== thrownErr) { + throw err; + } + } + ReactDOM.unmountComponentAtNode(container); + ReactPerf.stop(); + }); + + it('should not print errant warnings if portal throws in componentWillMount()', () => { + var container = document.createElement('div'); + var thrownErr = new Error('Muhaha!'); + + class Evil extends React.Component { + componentWillMount() { + throw thrownErr; + } + render() { + return
; + } + } + class EvilPortal extends React.Component { + componentWillMount() { + var portalContainer = document.createElement('div'); + ReactDOM.render(, portalContainer); + } + render() { + return
; + } + } + + ReactPerf.start(); + try { + ReactDOM.render( +
+ + +
, + container + ); + } catch (err) { + if (err !== thrownErr) { + throw err; + } + } + ReactDOM.unmountComponentAtNode(container); + ReactPerf.stop(); + }); + + it('should not print errant warnings if portal throws in componentDidMount()', () => { + var container = document.createElement('div'); + var thrownErr = new Error('Muhaha!'); + + class Evil extends React.Component { + componentDidMount() { + throw thrownErr; + } + render() { + return
; + } + } + class EvilPortal extends React.Component { + componentWillMount() { + var portalContainer = document.createElement('div'); + ReactDOM.render(, portalContainer); + } + render() { + return
; + } + } + + ReactPerf.start(); + try { + ReactDOM.render( +
+ + +
, + container + ); + } catch (err) { + if (err !== thrownErr) { + throw err; + } + } + ReactDOM.unmountComponentAtNode(container); + ReactPerf.stop(); + }); }); diff --git a/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js b/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js index bce59875a2e08..9536502084050 100644 --- a/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js +++ b/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js @@ -59,40 +59,6 @@ function warnIfInvalidElement(Component, element) { } } -function invokeComponentDidMountWithTimer() { - var publicInstance = this._instance; - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onBeginLifeCycleTimer( - this._debugID, - 'componentDidMount' - ); - } - publicInstance.componentDidMount(); - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onEndLifeCycleTimer( - this._debugID, - 'componentDidMount' - ); - } -} - -function invokeComponentDidUpdateWithTimer(prevProps, prevState, prevContext) { - var publicInstance = this._instance; - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onBeginLifeCycleTimer( - this._debugID, - 'componentDidUpdate' - ); - } - publicInstance.componentDidUpdate(prevProps, prevState, prevContext); - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onEndLifeCycleTimer( - this._debugID, - 'componentDidUpdate' - ); - } -} - function shouldConstruct(Component) { return !!(Component.prototype && Component.prototype.isReactComponent); } @@ -101,6 +67,23 @@ function isPureComponent(Component) { return !!(Component.prototype && Component.prototype.isPureReactComponent); } +// Separated into a function to contain deoptimizations caused by try/finally. +function measureLifeCyclePerf(fn, debugID, timerType) { + if (debugID === 0) { + // Top-level wrappers (see ReactMount) and empty components (see + // ReactDOMEmptyComponent) are invisible to hooks and devtools. + // Both are implementation details that should go away in the future. + return fn(); + } + + ReactInstrumentation.debugTool.onBeginLifeCycleTimer(debugID, timerType); + try { + return fn(); + } finally { + ReactInstrumentation.debugTool.onEndLifeCycleTimer(debugID, timerType); + } +} + /** * ------------------ The Life-Cycle of a Composite Component ------------------ * @@ -361,7 +344,13 @@ var ReactCompositeComponentMixin = { if (inst.componentDidMount) { if (__DEV__) { - transaction.getReactMountReady().enqueue(invokeComponentDidMountWithTimer, this); + transaction.getReactMountReady().enqueue(() => { + measureLifeCyclePerf( + () => inst.componentDidMount(), + this._debugID, + 'componentDidMount' + ); + }); } else { transaction.getReactMountReady().enqueue(inst.componentDidMount, inst); } @@ -405,47 +394,30 @@ var ReactCompositeComponentMixin = { updateQueue ) { var Component = this._currentElement.type; - var instanceOrElement; + if (doConstruct) { if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onBeginLifeCycleTimer( - this._debugID, - 'ctor' - ); - } - } - instanceOrElement = new Component(publicProps, publicContext, updateQueue); - if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onEndLifeCycleTimer( - this._debugID, - 'ctor' - ); - } + return measureLifeCyclePerf( + () => new Component(publicProps, publicContext, updateQueue), + this._debugID, + 'ctor' + ); + } else { + return new Component(publicProps, publicContext, updateQueue); } + } + + // This can still be an instance in case of factory components + // but we'll count this as time spent rendering as the more common case. + if (__DEV__) { + return measureLifeCyclePerf( + () => Component(publicProps, publicContext, updateQueue), + this._debugID, + 'render' + ); } else { - // This can still be an instance in case of factory components - // but we'll count this as time spent rendering as the more common case. - if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onBeginLifeCycleTimer( - this._debugID, - 'render' - ); - } - } - instanceOrElement = Component(publicProps, publicContext, updateQueue); - if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onEndLifeCycleTimer( - this._debugID, - 'render' - ); - } - } + return Component(publicProps, publicContext, updateQueue); } - return instanceOrElement; }, performInitialMountWithErrorHandling: function( @@ -460,11 +432,6 @@ var ReactCompositeComponentMixin = { try { markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context); } catch (e) { - if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onError(); - } - } // Roll back to checkpoint, handle error (which may add items to the transaction), and take a new checkpoint transaction.rollback(checkpoint); this._instance.unstable_handleError(e); @@ -485,23 +452,21 @@ var ReactCompositeComponentMixin = { performInitialMount: function(renderedElement, hostParent, hostContainerInfo, transaction, context) { var inst = this._instance; + + var debugID = 0; + if (__DEV__) { + debugID = this._debugID; + } + if (inst.componentWillMount) { if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onBeginLifeCycleTimer( - this._debugID, - 'componentWillMount' - ); - } - } - inst.componentWillMount(); - if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onEndLifeCycleTimer( - this._debugID, - 'componentWillMount' - ); - } + measureLifeCyclePerf( + () => inst.componentWillMount(), + debugID, + 'componentWillMount' + ); + } else { + inst.componentWillMount(); } // When mounting, calls to `setState` by `componentWillMount` will set // `this._pendingStateQueue` without triggering a re-render. @@ -523,25 +488,19 @@ var ReactCompositeComponentMixin = { ); this._renderedComponent = child; - var selfDebugID = 0; - if (__DEV__) { - selfDebugID = this._debugID; - } var markup = ReactReconciler.mountComponent( child, transaction, hostParent, hostContainerInfo, this._processChildContext(context), - selfDebugID + debugID ); if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onSetChildren( - this._debugID, - child._debugID !== 0 ? [child._debugID] : [] - ); + if (debugID !== 0) { + var childDebugIDs = child._debugID !== 0 ? [child._debugID] : []; + ReactInstrumentation.debugTool.onSetChildren(debugID, childDebugIDs); } } @@ -562,30 +521,24 @@ var ReactCompositeComponentMixin = { if (!this._renderedComponent) { return; } + var inst = this._instance; if (inst.componentWillUnmount && !inst._calledComponentWillUnmount) { inst._calledComponentWillUnmount = true; - if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onBeginLifeCycleTimer( - this._debugID, - 'componentWillUnmount' - ); - } - } + if (safely) { var name = this.getName() + '.componentWillUnmount()'; ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst)); } else { - inst.componentWillUnmount(); - } - if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onEndLifeCycleTimer( + if (__DEV__) { + measureLifeCyclePerf( + () => inst.componentWillUnmount(), this._debugID, 'componentWillUnmount' ); + } else { + inst.componentWillUnmount(); } } } @@ -676,13 +629,21 @@ var ReactCompositeComponentMixin = { _processChildContext: function(currentContext) { var Component = this._currentElement.type; var inst = this._instance; - if (__DEV__) { - ReactInstrumentation.debugTool.onBeginProcessingChildContext(); - } - var childContext = inst.getChildContext && inst.getChildContext(); - if (__DEV__) { - ReactInstrumentation.debugTool.onEndProcessingChildContext(); + var childContext; + + if (inst.getChildContext) { + if (__DEV__) { + ReactInstrumentation.debugTool.onBeginProcessingChildContext(); + try { + childContext = inst.getChildContext(); + } finally { + ReactInstrumentation.debugTool.onEndProcessingChildContext(); + } + } else { + childContext = inst.getChildContext(); + } } + if (childContext) { invariant( typeof Component.childContextTypes === 'object', @@ -826,21 +787,13 @@ var ReactCompositeComponentMixin = { // immediately reconciled instead of waiting for the next batch. if (willReceive && inst.componentWillReceiveProps) { if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onBeginLifeCycleTimer( - this._debugID, - 'componentWillReceiveProps' - ); - } - } - inst.componentWillReceiveProps(nextProps, nextContext); - if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onEndLifeCycleTimer( - this._debugID, - 'componentWillReceiveProps' - ); - } + measureLifeCyclePerf( + () => inst.componentWillReceiveProps(nextProps, nextContext), + this._debugID, + 'componentWillReceiveProps', + ); + } else { + inst.componentWillReceiveProps(nextProps, nextContext); } } @@ -850,21 +803,13 @@ var ReactCompositeComponentMixin = { if (!this._pendingForceUpdate) { if (inst.shouldComponentUpdate) { if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onBeginLifeCycleTimer( - this._debugID, - 'shouldComponentUpdate' - ); - } - } - shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext); - if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onEndLifeCycleTimer( - this._debugID, - 'shouldComponentUpdate' - ); - } + shouldUpdate = measureLifeCyclePerf( + () => inst.shouldComponentUpdate(nextProps, nextState, nextContext), + this._debugID, + 'shouldComponentUpdate' + ); + } else { + shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext); } } else { if (this._compositeType === CompositeTypes.PureClass) { @@ -970,21 +915,13 @@ var ReactCompositeComponentMixin = { if (inst.componentWillUpdate) { if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onBeginLifeCycleTimer( - this._debugID, - 'componentWillUpdate' - ); - } - } - inst.componentWillUpdate(nextProps, nextState, nextContext); - if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onEndLifeCycleTimer( - this._debugID, - 'componentWillUpdate' - ); - } + measureLifeCyclePerf( + () => inst.componentWillUpdate(nextProps, nextState, nextContext), + this._debugID, + 'componentWillUpdate' + ); + } else { + inst.componentWillUpdate(nextProps, nextState, nextContext); } } @@ -998,10 +935,13 @@ var ReactCompositeComponentMixin = { if (hasComponentDidUpdate) { if (__DEV__) { - transaction.getReactMountReady().enqueue( - invokeComponentDidUpdateWithTimer.bind(this, prevProps, prevState, prevContext), - this - ); + transaction.getReactMountReady().enqueue(() => { + measureLifeCyclePerf( + inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext), + this._debugID, + 'componentDidUpdate' + ); + }); } else { transaction.getReactMountReady().enqueue( inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext), @@ -1021,6 +961,12 @@ var ReactCompositeComponentMixin = { var prevComponentInstance = this._renderedComponent; var prevRenderedElement = prevComponentInstance._currentElement; var nextRenderedElement = this._renderValidatedComponent(); + + var debugID = 0; + if (__DEV__) { + debugID = this._debugID; + } + if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) { ReactReconciler.receiveComponent( prevComponentInstance, @@ -1040,25 +986,19 @@ var ReactCompositeComponentMixin = { ); this._renderedComponent = child; - var selfDebugID = 0; - if (__DEV__) { - selfDebugID = this._debugID; - } var nextMarkup = ReactReconciler.mountComponent( child, transaction, this._hostParent, this._hostContainerInfo, this._processChildContext(context), - selfDebugID + debugID ); if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onSetChildren( - this._debugID, - child._debugID !== 0 ? [child._debugID] : [] - ); + if (debugID !== 0) { + var childDebugIDs = child._debugID !== 0 ? [child._debugID] : []; + ReactInstrumentation.debugTool.onSetChildren(debugID, childDebugIDs); } } @@ -1088,23 +1028,16 @@ var ReactCompositeComponentMixin = { */ _renderValidatedComponentWithoutOwnerOrContext: function() { var inst = this._instance; + var renderedComponent; if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onBeginLifeCycleTimer( - this._debugID, - 'render' - ); - } - } - var renderedComponent = inst.render(); - if (__DEV__) { - if (this._debugID !== 0) { - ReactInstrumentation.debugTool.onEndLifeCycleTimer( - this._debugID, - 'render' - ); - } + renderedComponent = measureLifeCyclePerf( + () => inst.render(), + this._debugID, + 'render' + ); + } else { + renderedComponent = inst.render(); } if (__DEV__) {