diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index ab34e726913fa..761b7f8b65720 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -1165,6 +1165,7 @@ src/renderers/shared/fiber/__tests__/ReactIncremental-test.js * reads context when setState is above the provider * maintains the correct context when providers bail out due to low priority * maintains the correct context when unwinding due to an error in render +* should not recreate masked context unless inputs have changed src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js * catches render error in a boundary during full deferred mounting diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 128be59141b2b..2b438657e4c43 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -31,6 +31,7 @@ var { var ReactTypeOfWork = require('ReactTypeOfWork'); var { getMaskedContext, + getUnmaskedContext, hasContextChanged, pushContextProvider, pushTopLevelContextObject, @@ -210,7 +211,8 @@ module.exports = function( return bailoutOnAlreadyFinishedWork(current, workInProgress); } - var context = getMaskedContext(workInProgress); + var unmaskedContext = getUnmaskedContext(workInProgress); + var context = getMaskedContext(workInProgress, unmaskedContext); var nextChildren; @@ -427,7 +429,8 @@ module.exports = function( } var fn = workInProgress.type; var props = workInProgress.pendingProps; - var context = getMaskedContext(workInProgress); + var unmaskedContext = getUnmaskedContext(workInProgress); + var context = getMaskedContext(workInProgress, unmaskedContext); var value; diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 99e9a08416a9c..981ca661f876b 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -17,6 +17,7 @@ import type { PriorityLevel } from 'ReactPriorityLevel'; var { getMaskedContext, + getUnmaskedContext, } = require('ReactFiberContext'); var { addUpdate, @@ -204,10 +205,17 @@ module.exports = function( function constructClassInstance(workInProgress : Fiber) : any { const ctor = workInProgress.type; const props = workInProgress.pendingProps; - const context = getMaskedContext(workInProgress); + const unmaskedContext = getUnmaskedContext(workInProgress); + const context = getMaskedContext(workInProgress, unmaskedContext); const instance = new ctor(props, context); adoptClassInstance(workInProgress, instance); checkClassInstance(workInProgress); + + // Cache unmasked context so we can avoid recreating masked context unless necessary. + // ReactFiberContext usually updates this cache but can't for newly-created instances. + instance.__reactInternalMemoizedUnmaskedChildContext = unmaskedContext; + instance.__reactInternalMemoizedMaskedChildContext = context; + return instance; } @@ -221,9 +229,11 @@ module.exports = function( throw new Error('There must be pending props for an initial mount.'); } + const unmaskedContext = getUnmaskedContext(workInProgress); + instance.props = props; instance.state = state; - instance.context = getMaskedContext(workInProgress); + instance.context = getMaskedContext(workInProgress, unmaskedContext); if (typeof instance.componentWillMount === 'function') { instance.componentWillMount(); @@ -256,7 +266,8 @@ module.exports = function( throw new Error('There should always be pending or memoized props.'); } } - const newContext = getMaskedContext(workInProgress); + const newUnmaskedContext = getUnmaskedContext(workInProgress); + const newContext = getMaskedContext(workInProgress, newUnmaskedContext); // TODO: Should we deal with a setState that happened after the last // componentWillMount and before this componentWillMount? Probably @@ -277,7 +288,7 @@ module.exports = function( const newInstance = constructClassInstance(workInProgress); newInstance.props = newProps; newInstance.state = newState = newInstance.state || null; - newInstance.context = getMaskedContext(workInProgress); + newInstance.context = newContext; if (typeof newInstance.componentWillMount === 'function') { newInstance.componentWillMount(); @@ -314,7 +325,8 @@ module.exports = function( } } const oldContext = instance.context; - const newContext = getMaskedContext(workInProgress); + const newUnmaskedContext = getUnmaskedContext(workInProgress); + const newContext = getMaskedContext(workInProgress, newUnmaskedContext); // Note: During these life-cycles, instance.props/instance.state are what // ever the previously attempted to render - not the "current". However, diff --git a/src/renderers/shared/fiber/ReactFiberContext.js b/src/renderers/shared/fiber/ReactFiberContext.js index a073b5280d89b..17cbecb793e00 100644 --- a/src/renderers/shared/fiber/ReactFiberContext.js +++ b/src/renderers/shared/fiber/ReactFiberContext.js @@ -55,15 +55,26 @@ function getUnmaskedContext(workInProgress : Fiber) : Object { } return contextStackCursor.current; } +exports.getUnmaskedContext = getUnmaskedContext; -exports.getMaskedContext = function(workInProgress : Fiber) { +exports.getMaskedContext = function(workInProgress : Fiber, unmaskedContext : Object) { const type = workInProgress.type; const contextTypes = type.contextTypes; if (!contextTypes) { return emptyObject; } - const unmaskedContext = getUnmaskedContext(workInProgress); + // Avoid recreating masked context unless unmasked context has changed. + // Failing to do this will result in unnecessary calls to componentWillReceiveProps. + // This may trigger infinite loops if componentWillReceiveProps calls setState. + const instance = workInProgress.stateNode; + if ( + instance && + instance.__reactInternalMemoizedUnmaskedChildContext === unmaskedContext + ) { + return instance.__reactInternalMemoizedMaskedChildContext; + } + const context = {}; for (let key in contextTypes) { context[key] = unmaskedContext[key]; @@ -74,6 +85,12 @@ exports.getMaskedContext = function(workInProgress : Fiber) { checkReactTypeSpec(contextTypes, context, 'context', name, null, workInProgress); } + // Cache unmasked context so we can avoid recreating masked context unless necessary. + if (instance) { + instance.__reactInternalMemoizedUnmaskedChildContext = unmaskedContext; + instance.__reactInternalMemoizedMaskedChildContext = context; + } + return context; }; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index defd5e672b230..e356056af4c8f 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -2021,4 +2021,50 @@ describe('ReactIncremental', () => { }); ReactNoop.flush(); }); + + it('should not recreate masked context unless inputs have changed', () => { + const ops = []; + + let scuCounter = 0; + + class MyComponent extends React.Component { + static contextTypes = {}; + componentDidMount(prevProps, prevState) { + ops.push('componentDidMount'); + this.setState({ setStateInCDU: true }); + } + componentDidUpdate(prevProps, prevState) { + ops.push('componentDidUpdate'); + if (this.state.setStateInCDU) { + this.setState({ setStateInCDU: false }); + } + } + componentWillReceiveProps(nextProps) { + ops.push('componentWillReceiveProps'); + this.setState({ setStateInCDU: true }); + } + render() { + ops.push('render'); + return null; + } + shouldComponentUpdate(nextProps, nextState) { + ops.push('shouldComponentUpdate'); + return scuCounter++ < 5; // Don't let test hang + } + } + + ReactNoop.render(); + ReactNoop.flush(); + + expect(ops).toEqual([ + 'render', + 'componentDidMount', + 'shouldComponentUpdate', + 'render', + 'componentDidUpdate', + 'shouldComponentUpdate', + 'render', + 'componentDidUpdate', + ]); + }); });