diff --git a/src/isomorphic/modern/class/__tests__/ReactPureComponent-test.js b/src/isomorphic/modern/class/__tests__/ReactPureComponent-test.js index b0411626b9f66..6903a4e34bbd5 100644 --- a/src/isomorphic/modern/class/__tests__/ReactPureComponent-test.js +++ b/src/isomorphic/modern/class/__tests__/ReactPureComponent-test.js @@ -75,4 +75,113 @@ describe('ReactPureComponent', function() { expect(renders).toBe(2); }); + it('does not update functional components inside pure components', function() { + // Multiple levels of host components and functional components; make sure + // purity propagates down. So we render: + // + // + // + // + // + // + // + // + // with some host wrappers in between. The render code is a little + // convoluted because we want to make the props scalar-equal as long as + // `text` (threaded through the whole tree) is. The outer two Functional + // components should always rerender; the inner Functional components should + // only rerender if `text` changes to a different object. + + var impureRenders = 0; + var pureRenders = 0; + var functionalRenders = 0; + + var pureComponent; + class Impure extends React.Component { + render() { + impureRenders++; + return ( +
+ {/* These props will always be shallow-equal. */} + +
+ ); + } + } + class Pure extends React.PureComponent { + render() { + pureComponent = this; + pureRenders++; + return ( +
+ +
+ ); + } + } + function Functional(props) { + functionalRenders++; + if (props.depth <= 1) { + return ( +
+ {props.prefix} + {props.thenRender === 'pureComponent' ? + [props.text[0] + '/', ] : + props.text[0]} +
+ ); + } else { + return ( +
+ +
+ ); + } + } + + var container = document.createElement('div'); + var text; + + text = ['porcini']; + ReactDOM.render(, container); + expect(container.textContent).toBe('porcini/porcini'); + expect(impureRenders).toBe(1); + expect(pureRenders).toBe(1); + expect(functionalRenders).toBe(4); + + text = ['morel']; + ReactDOM.render(, container); + expect(container.textContent).toBe('morel/morel'); + expect(impureRenders).toBe(2); + expect(pureRenders).toBe(2); + expect(functionalRenders).toBe(8); + + text[0] = 'portobello'; + ReactDOM.render(, container); + // Updates happen down and stop at the pure component + expect(container.textContent).toBe('portobello/morel'); + expect(impureRenders).toBe(3); + expect(pureRenders).toBe(2); + expect(functionalRenders).toBe(10); + + // Forcing the pure component to update makes it rerender, but its + // functional children still don't. + pureComponent.forceUpdate(); + expect(container.textContent).toBe('portobello/morel'); + expect(impureRenders).toBe(3); + expect(pureRenders).toBe(3); + expect(functionalRenders).toBe(10); + }); + }); diff --git a/src/renderers/dom/shared/ReactDOMComponent.js b/src/renderers/dom/shared/ReactDOMComponent.js index 5885c1f6d0575..f8e9b6f4a228c 100644 --- a/src/renderers/dom/shared/ReactDOMComponent.js +++ b/src/renderers/dom/shared/ReactDOMComponent.js @@ -846,10 +846,16 @@ ReactDOMComponent.Mixin = { * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction * @param {object} context */ - receiveComponent: function(nextElement, transaction, context) { + receiveComponent: function(nextElement, transaction, context, isParentPure) { var prevElement = this._currentElement; this._currentElement = nextElement; - this.updateComponent(transaction, prevElement, nextElement, context); + this.updateComponent( + transaction, + prevElement, + nextElement, + context, + isParentPure + ); }, /** @@ -862,7 +868,13 @@ ReactDOMComponent.Mixin = { * @internal * @overridable */ - updateComponent: function(transaction, prevElement, nextElement, context) { + updateComponent: function( + transaction, + prevElement, + nextElement, + context, + isParentPure + ) { var lastProps = prevElement.props; var nextProps = this._currentElement.props; @@ -897,7 +909,8 @@ ReactDOMComponent.Mixin = { lastProps, nextProps, transaction, - context + context, + isParentPure ); if (this._tag === 'select') { @@ -1053,7 +1066,13 @@ ReactDOMComponent.Mixin = { * @param {ReactReconcileTransaction} transaction * @param {object} context */ - _updateDOMChildren: function(lastProps, nextProps, transaction, context) { + _updateDOMChildren: function( + lastProps, + nextProps, + transaction, + context, + isParentPure + ) { var lastContent = CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null; var nextContent = @@ -1075,7 +1094,7 @@ ReactDOMComponent.Mixin = { var lastHasContentOrHtml = lastContent != null || lastHtml != null; var nextHasContentOrHtml = nextContent != null || nextHtml != null; if (lastChildren != null && nextChildren == null) { - this.updateChildren(null, transaction, context); + this.updateChildren(null, transaction, context, isParentPure); } else if (lastHasContentOrHtml && !nextHasContentOrHtml) { this.updateTextContent(''); if (__DEV__) { @@ -1102,7 +1121,7 @@ ReactDOMComponent.Mixin = { setContentChildForInstrumentation.call(this, null); } - this.updateChildren(nextChildren, transaction, context); + this.updateChildren(nextChildren, transaction, context, isParentPure); } }, diff --git a/src/renderers/dom/shared/ReactDOMEmptyComponent.js b/src/renderers/dom/shared/ReactDOMEmptyComponent.js index d77ea340582bd..29f17cd6c6599 100644 --- a/src/renderers/dom/shared/ReactDOMEmptyComponent.js +++ b/src/renderers/dom/shared/ReactDOMEmptyComponent.js @@ -52,7 +52,7 @@ Object.assign(ReactDOMEmptyComponent.prototype, { return ''; } }, - receiveComponent: function() { + receiveComponent: function(nextElement, transaction, context, isParentPure) { }, getHostNode: function() { return ReactDOMComponentTree.getNodeFromInstance(this); diff --git a/src/renderers/dom/shared/ReactDOMTextComponent.js b/src/renderers/dom/shared/ReactDOMTextComponent.js index 95eacc9c2c06a..c9dc87c3c542d 100644 --- a/src/renderers/dom/shared/ReactDOMTextComponent.js +++ b/src/renderers/dom/shared/ReactDOMTextComponent.js @@ -127,7 +127,7 @@ Object.assign(ReactDOMTextComponent.prototype, { * @param {ReactReconcileTransaction} transaction * @internal */ - receiveComponent: function(nextText, transaction) { + receiveComponent: function(nextText, transaction, context, isParentPure) { if (nextText !== this._currentElement) { this._currentElement = nextText; var nextStringText = '' + nextText; diff --git a/src/renderers/native/ReactNativeBaseComponent.js b/src/renderers/native/ReactNativeBaseComponent.js index 20dfaa542b311..d4ea7fe1a4ee6 100644 --- a/src/renderers/native/ReactNativeBaseComponent.js +++ b/src/renderers/native/ReactNativeBaseComponent.js @@ -97,7 +97,7 @@ ReactNativeBaseComponent.Mixin = { * @param {object} context * @internal */ - receiveComponent: function(nextElement, transaction, context) { + receiveComponent: function(nextElement, transaction, context, isParentPure) { var prevElement = this._currentElement; this._currentElement = nextElement; @@ -127,7 +127,12 @@ ReactNativeBaseComponent.Mixin = { prevElement.props, nextElement.props ); - this.updateChildren(nextElement.props.children, transaction, context); + this.updateChildren( + nextElement.props.children, + transaction, + context, + isParentPure + ); }, /** diff --git a/src/renderers/native/ReactNativeTextComponent.js b/src/renderers/native/ReactNativeTextComponent.js index 2195923941c15..f2ee98eb5fbfe 100644 --- a/src/renderers/native/ReactNativeTextComponent.js +++ b/src/renderers/native/ReactNativeTextComponent.js @@ -59,7 +59,7 @@ Object.assign(ReactNativeTextComponent.prototype, { return this._rootNodeID; }, - receiveComponent: function(nextText, transaction, context) { + receiveComponent: function(nextText, transaction, context, isParentPure) { if (nextText !== this._currentElement) { this._currentElement = nextText; var nextStringText = '' + nextText; diff --git a/src/renderers/shared/stack/reconciler/ReactChildReconciler.js b/src/renderers/shared/stack/reconciler/ReactChildReconciler.js index ee1ce8217a161..c58585d3296ed 100644 --- a/src/renderers/shared/stack/reconciler/ReactChildReconciler.js +++ b/src/renderers/shared/stack/reconciler/ReactChildReconciler.js @@ -95,7 +95,8 @@ var ReactChildReconciler = { nextChildren, removedNodes, transaction, - context) { + context, + isParentPure) { // We currently don't have a way to track moves here but if we use iterators // instead of for..in we can zip the iterators and check if an item has // moved. @@ -116,7 +117,7 @@ var ReactChildReconciler = { if (prevChild != null && shouldUpdateReactComponent(prevElement, nextElement)) { ReactReconciler.receiveComponent( - prevChild, nextElement, transaction, context + prevChild, nextElement, transaction, context, isParentPure ); nextChildren[name] = prevChild; } else { diff --git a/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js b/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js index 28a2b60c5e212..79703918cc18b 100644 --- a/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js +++ b/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js @@ -694,7 +694,12 @@ var ReactCompositeComponentMixin = { ); }, - receiveComponent: function(nextElement, transaction, nextContext) { + receiveComponent: function( + nextElement, + transaction, + nextContext, + isParentPure + ) { var prevElement = this._currentElement; var prevContext = this._context; @@ -705,7 +710,8 @@ var ReactCompositeComponentMixin = { prevElement, nextElement, prevContext, - nextContext + nextContext, + isParentPure ); }, @@ -722,7 +728,10 @@ var ReactCompositeComponentMixin = { this, this._pendingElement, transaction, - this._context + this._context, + // Element updates are enqueued only at the top level, which we consider + // impure + false ); } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) { this.updateComponent( @@ -730,7 +739,10 @@ var ReactCompositeComponentMixin = { this._currentElement, this._currentElement, this._context, - this._context + this._context, + // isParentPure here doesn't matter because state updates don't happen to + // functional components. + true ); } else { this._updateBatchNumber = null; @@ -757,7 +769,8 @@ var ReactCompositeComponentMixin = { prevParentElement, nextParentElement, prevUnmaskedContext, - nextUnmaskedContext + nextUnmaskedContext, + isParentPure ) { var inst = this._instance; var willReceive = false; @@ -805,6 +818,9 @@ var ReactCompositeComponentMixin = { var nextState = this._processPendingState(nextProps, nextContext); var shouldUpdate = true; + var pureSelf = + this._compositeType === CompositeTypes.PureClass || + isParentPure && this._compositeType === CompositeTypes.StatelessFunctional; if (!this._pendingForceUpdate) { if (inst.shouldComponentUpdate) { if (__DEV__) { @@ -825,7 +841,7 @@ var ReactCompositeComponentMixin = { } } } else { - if (this._compositeType === CompositeTypes.PureClass) { + if (pureSelf) { shouldUpdate = inst.state !== nextState || !shallowEqual(prevProps, nextProps); } @@ -851,7 +867,8 @@ var ReactCompositeComponentMixin = { nextState, nextContext, transaction, - nextUnmaskedContext + nextUnmaskedContext, + pureSelf ); } else { // If it's determined that a component should not update, we still want @@ -911,7 +928,8 @@ var ReactCompositeComponentMixin = { nextState, nextContext, transaction, - unmaskedContext + unmaskedContext, + pureSelf ) { var inst = this._instance; @@ -951,7 +969,7 @@ var ReactCompositeComponentMixin = { inst.state = nextState; inst.context = nextContext; - this._updateRenderedComponent(transaction, unmaskedContext); + this._updateRenderedComponent(transaction, unmaskedContext, pureSelf); if (hasComponentDidUpdate) { if (__DEV__) { @@ -974,7 +992,7 @@ var ReactCompositeComponentMixin = { * @param {ReactReconcileTransaction} transaction * @internal */ - _updateRenderedComponent: function(transaction, context) { + _updateRenderedComponent: function(transaction, context, pureSelf) { var prevComponentInstance = this._renderedComponent; var prevRenderedElement = prevComponentInstance._currentElement; var nextRenderedElement = this._renderValidatedComponent(); @@ -983,7 +1001,8 @@ var ReactCompositeComponentMixin = { prevComponentInstance, nextRenderedElement, transaction, - this._processChildContext(context) + this._processChildContext(context), + pureSelf ); } else { var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance); diff --git a/src/renderers/shared/stack/reconciler/ReactMultiChild.js b/src/renderers/shared/stack/reconciler/ReactMultiChild.js index 96085235c78b1..8beb1602905e5 100644 --- a/src/renderers/shared/stack/reconciler/ReactMultiChild.js +++ b/src/renderers/shared/stack/reconciler/ReactMultiChild.js @@ -209,7 +209,8 @@ var ReactMultiChild = { nextNestedChildrenElements, removedNodes, transaction, - context + context, + isParentPure ) { var nextChildren; if (__DEV__) { @@ -221,14 +222,24 @@ var ReactMultiChild = { ReactCurrentOwner.current = null; } ReactChildReconciler.updateChildren( - prevChildren, nextChildren, removedNodes, transaction, context + prevChildren, + nextChildren, + removedNodes, + transaction, + context, + isParentPure ); return nextChildren; } } nextChildren = flattenChildren(nextNestedChildrenElements); ReactChildReconciler.updateChildren( - prevChildren, nextChildren, removedNodes, transaction, context + prevChildren, + nextChildren, + removedNodes, + transaction, + context, + isParentPure ); return nextChildren; }, @@ -320,9 +331,19 @@ var ReactMultiChild = { * @param {ReactReconcileTransaction} transaction * @internal */ - updateChildren: function(nextNestedChildrenElements, transaction, context) { + updateChildren: function( + nextNestedChildrenElements, + transaction, + context, + isParentPure + ) { // Hook used by React ART - this._updateChildren(nextNestedChildrenElements, transaction, context); + this._updateChildren( + nextNestedChildrenElements, + transaction, + context, + isParentPure + ); }, /** @@ -331,7 +352,12 @@ var ReactMultiChild = { * @final * @protected */ - _updateChildren: function(nextNestedChildrenElements, transaction, context) { + _updateChildren: function( + nextNestedChildrenElements, + transaction, + context, + isParentPure + ) { var prevChildren = this._renderedChildren; var removedNodes = {}; var nextChildren = this._reconcilerUpdateChildren( @@ -339,7 +365,8 @@ var ReactMultiChild = { nextNestedChildrenElements, removedNodes, transaction, - context + context, + isParentPure ); if (!nextChildren && !prevChildren) { return; diff --git a/src/renderers/shared/stack/reconciler/ReactReconciler.js b/src/renderers/shared/stack/reconciler/ReactReconciler.js index f9e6473b7a77a..bbc74f37d3b01 100644 --- a/src/renderers/shared/stack/reconciler/ReactReconciler.js +++ b/src/renderers/shared/stack/reconciler/ReactReconciler.js @@ -128,7 +128,7 @@ var ReactReconciler = { * @internal */ receiveComponent: function( - internalInstance, nextElement, transaction, context + internalInstance, nextElement, transaction, context, isParentPure ) { var prevElement = internalInstance._currentElement; @@ -170,7 +170,12 @@ var ReactReconciler = { ReactRef.detachRefs(internalInstance, prevElement); } - internalInstance.receiveComponent(nextElement, transaction, context); + internalInstance.receiveComponent( + nextElement, + transaction, + context, + isParentPure + ); if (refsChanged && internalInstance._currentElement && diff --git a/src/renderers/shared/stack/reconciler/ReactSimpleEmptyComponent.js b/src/renderers/shared/stack/reconciler/ReactSimpleEmptyComponent.js index 54fe5e0103d6e..b8a24167bde4b 100644 --- a/src/renderers/shared/stack/reconciler/ReactSimpleEmptyComponent.js +++ b/src/renderers/shared/stack/reconciler/ReactSimpleEmptyComponent.js @@ -33,7 +33,7 @@ Object.assign(ReactSimpleEmptyComponent.prototype, { context ); }, - receiveComponent: function() { + receiveComponent: function(nextElement, transaction, context, isParentPure) { }, getHostNode: function() { return ReactReconciler.getHostNode(this._renderedComponent); diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index 093bfad0dd32d..3276f5824c9f3 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -391,7 +391,7 @@ NoopInternalComponent.prototype = { mountComponent: function() { }, - receiveComponent: function(element) { + receiveComponent: function(element, transaction, context, isParentPure) { this._renderedOutput = element; this._currentElement = element; }, @@ -487,7 +487,8 @@ ReactShallowRenderer.prototype._render = function(element, transaction, context) this._instance, element, transaction, - context + context, + /* isParentPure: */ false ); } else { var instance = new ShallowComponentWrapper(element);