Skip to content

Commit 0af8c44

Browse files
committed
Inherit purity for functional components
We've heard clearly that most React users intend for their functional components to be pure and to produce different output only if the component's props have changed (https://mobile.twitter.com/reactjs/status/736412808372314114). However, a significant fraction of users still rely on mutation in their apps; when used with mutation, comparing props on each render could lead to components not updating even when the data is changed. Therefore, we're changing functional components to behave as pure when they're used inside a React.PureComponent but to rerender unconditionally when contained in a React.Component: ```js class Post extends React.PureComponent { // or React.Component render() { return ( <div className="post"> <PostHeader model={this.props.model} /> <PostBody model={this.props.model} /> </div> ); } } function PostHeader(props) { // ... } function PostBody(props) { // ... } ``` In this example, the functional components PostHeader and PostBody will be treated as pure because they're rendered by a pure parent component (Post). If our app used mutable models instead, Post should extend React.Component, which would cause PostHeader and PostBody to rerender whenever Post does, even if the model object is the same. We anticipate that this behavior will work well in real-world apps: if you use immutable data, your class-based components can extend React.PureComponent and your functional components will be pure too; if you use mutable data, your class-based components will extend React.Component and your functional components will update accordingly. In the future, we might adjust these heuristics to improve performance. For example, we might do runtime detection of components like ```js function FancyButton(props) { return <Button style="fancy" text={props.text} />; } ``` and optimize them to "inline" the child Button component and call it immediately, so that React doesn't need to store the props for Button nor allocate a backing instance for it -- causing less work to be performed and reducing GC pressure.
1 parent fdb84f4 commit 0af8c44

12 files changed

+223
-37
lines changed

src/isomorphic/modern/class/__tests__/ReactPureComponent-test.js

+109
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,113 @@ describe('ReactPureComponent', function() {
7575
expect(renders).toBe(2);
7676
});
7777

78+
it('does not update functional components inside pure components', function() {
79+
// Multiple levels of host components and functional components; make sure
80+
// purity propagates down. So we render:
81+
//
82+
// <Impure>
83+
// <Functional>
84+
// <Functional>
85+
// <Pure>
86+
// <Functional>
87+
// <Functional>
88+
//
89+
// with some host wrappers in between. The render code is a little
90+
// convoluted because we want to make the props scalar-equal as long as
91+
// `text` (threaded through the whole tree) is. The outer two Functional
92+
// components should always rerender; the inner Functional components should
93+
// only rerender if `text` changes to a different object.
94+
95+
var impureRenders = 0;
96+
var pureRenders = 0;
97+
var functionalRenders = 0;
98+
99+
var pureComponent;
100+
class Impure extends React.Component {
101+
render() {
102+
impureRenders++;
103+
return (
104+
<div>
105+
{/* These props will always be shallow-equal. */}
106+
<Functional
107+
depth={2}
108+
thenRender="pureComponent"
109+
text={this.props.text}
110+
/>
111+
</div>
112+
);
113+
}
114+
}
115+
class Pure extends React.PureComponent {
116+
render() {
117+
pureComponent = this;
118+
pureRenders++;
119+
return (
120+
<div>
121+
<Functional
122+
depth={2}
123+
thenRender="text"
124+
text={this.props.text}
125+
/>
126+
</div>
127+
);
128+
}
129+
}
130+
function Functional(props) {
131+
functionalRenders++;
132+
if (props.depth <= 1) {
133+
return (
134+
<div>
135+
{props.prefix}
136+
{props.thenRender === 'pureComponent' ?
137+
[props.text[0] + '/', <Pure key="pure" text={props.text} />] :
138+
props.text[0]}
139+
</div>
140+
);
141+
} else {
142+
return (
143+
<div>
144+
<Functional
145+
{...props}
146+
depth={props.depth - 1}
147+
/>
148+
</div>
149+
);
150+
}
151+
}
152+
153+
var container = document.createElement('div');
154+
var text;
155+
156+
text = ['porcini'];
157+
ReactDOM.render(<Impure text={text} />, container);
158+
expect(container.textContent).toBe('porcini/porcini');
159+
expect(impureRenders).toBe(1);
160+
expect(pureRenders).toBe(1);
161+
expect(functionalRenders).toBe(4);
162+
163+
text = ['morel'];
164+
ReactDOM.render(<Impure text={text} />, container);
165+
expect(container.textContent).toBe('morel/morel');
166+
expect(impureRenders).toBe(2);
167+
expect(pureRenders).toBe(2);
168+
expect(functionalRenders).toBe(8);
169+
170+
text[0] = 'portobello';
171+
ReactDOM.render(<Impure text={text} />, container);
172+
// Updates happen down and stop at the pure component
173+
expect(container.textContent).toBe('portobello/morel');
174+
expect(impureRenders).toBe(3);
175+
expect(pureRenders).toBe(2);
176+
expect(functionalRenders).toBe(10);
177+
178+
// Forcing the pure component to update makes it rerender, but its
179+
// functional children still don't.
180+
pureComponent.forceUpdate();
181+
expect(container.textContent).toBe('portobello/morel');
182+
expect(impureRenders).toBe(3);
183+
expect(pureRenders).toBe(3);
184+
expect(functionalRenders).toBe(10);
185+
});
186+
78187
});

src/renderers/dom/shared/ReactDOMComponent.js

+26-7
Original file line numberDiff line numberDiff line change
@@ -846,10 +846,16 @@ ReactDOMComponent.Mixin = {
846846
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
847847
* @param {object} context
848848
*/
849-
receiveComponent: function(nextElement, transaction, context) {
849+
receiveComponent: function(nextElement, transaction, context, isParentPure) {
850850
var prevElement = this._currentElement;
851851
this._currentElement = nextElement;
852-
this.updateComponent(transaction, prevElement, nextElement, context);
852+
this.updateComponent(
853+
transaction,
854+
prevElement,
855+
nextElement,
856+
context,
857+
isParentPure
858+
);
853859
},
854860

855861
/**
@@ -862,7 +868,13 @@ ReactDOMComponent.Mixin = {
862868
* @internal
863869
* @overridable
864870
*/
865-
updateComponent: function(transaction, prevElement, nextElement, context) {
871+
updateComponent: function(
872+
transaction,
873+
prevElement,
874+
nextElement,
875+
context,
876+
isParentPure
877+
) {
866878
var lastProps = prevElement.props;
867879
var nextProps = this._currentElement.props;
868880

@@ -897,7 +909,8 @@ ReactDOMComponent.Mixin = {
897909
lastProps,
898910
nextProps,
899911
transaction,
900-
context
912+
context,
913+
isParentPure
901914
);
902915

903916
if (this._tag === 'select') {
@@ -1053,7 +1066,13 @@ ReactDOMComponent.Mixin = {
10531066
* @param {ReactReconcileTransaction} transaction
10541067
* @param {object} context
10551068
*/
1056-
_updateDOMChildren: function(lastProps, nextProps, transaction, context) {
1069+
_updateDOMChildren: function(
1070+
lastProps,
1071+
nextProps,
1072+
transaction,
1073+
context,
1074+
isParentPure
1075+
) {
10571076
var lastContent =
10581077
CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
10591078
var nextContent =
@@ -1075,7 +1094,7 @@ ReactDOMComponent.Mixin = {
10751094
var lastHasContentOrHtml = lastContent != null || lastHtml != null;
10761095
var nextHasContentOrHtml = nextContent != null || nextHtml != null;
10771096
if (lastChildren != null && nextChildren == null) {
1078-
this.updateChildren(null, transaction, context);
1097+
this.updateChildren(null, transaction, context, isParentPure);
10791098
} else if (lastHasContentOrHtml && !nextHasContentOrHtml) {
10801099
this.updateTextContent('');
10811100
if (__DEV__) {
@@ -1102,7 +1121,7 @@ ReactDOMComponent.Mixin = {
11021121
setContentChildForInstrumentation.call(this, null);
11031122
}
11041123

1105-
this.updateChildren(nextChildren, transaction, context);
1124+
this.updateChildren(nextChildren, transaction, context, isParentPure);
11061125
}
11071126
},
11081127

src/renderers/dom/shared/ReactDOMEmptyComponent.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Object.assign(ReactDOMEmptyComponent.prototype, {
5252
return '<!--' + nodeValue + '-->';
5353
}
5454
},
55-
receiveComponent: function() {
55+
receiveComponent: function(nextElement, transaction, context, isParentPure) {
5656
},
5757
getHostNode: function() {
5858
return ReactDOMComponentTree.getNodeFromInstance(this);

src/renderers/dom/shared/ReactDOMTextComponent.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ Object.assign(ReactDOMTextComponent.prototype, {
127127
* @param {ReactReconcileTransaction} transaction
128128
* @internal
129129
*/
130-
receiveComponent: function(nextText, transaction) {
130+
receiveComponent: function(nextText, transaction, context, isParentPure) {
131131
if (nextText !== this._currentElement) {
132132
this._currentElement = nextText;
133133
var nextStringText = '' + nextText;

src/renderers/native/ReactNativeBaseComponent.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ ReactNativeBaseComponent.Mixin = {
9797
* @param {object} context
9898
* @internal
9999
*/
100-
receiveComponent: function(nextElement, transaction, context) {
100+
receiveComponent: function(nextElement, transaction, context, isParentPure) {
101101
var prevElement = this._currentElement;
102102
this._currentElement = nextElement;
103103

@@ -127,7 +127,12 @@ ReactNativeBaseComponent.Mixin = {
127127
prevElement.props,
128128
nextElement.props
129129
);
130-
this.updateChildren(nextElement.props.children, transaction, context);
130+
this.updateChildren(
131+
nextElement.props.children,
132+
transaction,
133+
context,
134+
isParentPure
135+
);
131136
},
132137

133138
/**

src/renderers/native/ReactNativeTextComponent.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Object.assign(ReactNativeTextComponent.prototype, {
5959
return this._rootNodeID;
6060
},
6161

62-
receiveComponent: function(nextText, transaction, context) {
62+
receiveComponent: function(nextText, transaction, context, isParentPure) {
6363
if (nextText !== this._currentElement) {
6464
this._currentElement = nextText;
6565
var nextStringText = '' + nextText;

src/renderers/shared/stack/reconciler/ReactChildReconciler.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ var ReactChildReconciler = {
9595
nextChildren,
9696
removedNodes,
9797
transaction,
98-
context) {
98+
context,
99+
isParentPure) {
99100
// We currently don't have a way to track moves here but if we use iterators
100101
// instead of for..in we can zip the iterators and check if an item has
101102
// moved.
@@ -116,7 +117,7 @@ var ReactChildReconciler = {
116117
if (prevChild != null &&
117118
shouldUpdateReactComponent(prevElement, nextElement)) {
118119
ReactReconciler.receiveComponent(
119-
prevChild, nextElement, transaction, context
120+
prevChild, nextElement, transaction, context, isParentPure
120121
);
121122
nextChildren[name] = prevChild;
122123
} else {

src/renderers/shared/stack/reconciler/ReactCompositeComponent.js

+30-11
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,12 @@ var ReactCompositeComponentMixin = {
694694
);
695695
},
696696

697-
receiveComponent: function(nextElement, transaction, nextContext) {
697+
receiveComponent: function(
698+
nextElement,
699+
transaction,
700+
nextContext,
701+
isParentPure
702+
) {
698703
var prevElement = this._currentElement;
699704
var prevContext = this._context;
700705

@@ -705,7 +710,8 @@ var ReactCompositeComponentMixin = {
705710
prevElement,
706711
nextElement,
707712
prevContext,
708-
nextContext
713+
nextContext,
714+
isParentPure
709715
);
710716
},
711717

@@ -722,15 +728,21 @@ var ReactCompositeComponentMixin = {
722728
this,
723729
this._pendingElement,
724730
transaction,
725-
this._context
731+
this._context,
732+
// Element updates are enqueued only at the top level, which we consider
733+
// impure
734+
false
726735
);
727736
} else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
728737
this.updateComponent(
729738
transaction,
730739
this._currentElement,
731740
this._currentElement,
732741
this._context,
733-
this._context
742+
this._context,
743+
// isParentPure here doesn't matter because state updates don't happen to
744+
// functional components.
745+
true
734746
);
735747
} else {
736748
this._updateBatchNumber = null;
@@ -757,7 +769,8 @@ var ReactCompositeComponentMixin = {
757769
prevParentElement,
758770
nextParentElement,
759771
prevUnmaskedContext,
760-
nextUnmaskedContext
772+
nextUnmaskedContext,
773+
isParentPure
761774
) {
762775
var inst = this._instance;
763776
var willReceive = false;
@@ -805,6 +818,9 @@ var ReactCompositeComponentMixin = {
805818
var nextState = this._processPendingState(nextProps, nextContext);
806819
var shouldUpdate = true;
807820

821+
var pureSelf =
822+
this._compositeType === CompositeTypes.PureClass ||
823+
isParentPure && this._compositeType === CompositeTypes.StatelessFunctional;
808824
if (!this._pendingForceUpdate) {
809825
if (inst.shouldComponentUpdate) {
810826
if (__DEV__) {
@@ -825,7 +841,7 @@ var ReactCompositeComponentMixin = {
825841
}
826842
}
827843
} else {
828-
if (this._compositeType === CompositeTypes.PureClass) {
844+
if (pureSelf) {
829845
shouldUpdate =
830846
inst.state !== nextState || !shallowEqual(prevProps, nextProps);
831847
}
@@ -851,7 +867,8 @@ var ReactCompositeComponentMixin = {
851867
nextState,
852868
nextContext,
853869
transaction,
854-
nextUnmaskedContext
870+
nextUnmaskedContext,
871+
pureSelf
855872
);
856873
} else {
857874
// If it's determined that a component should not update, we still want
@@ -911,7 +928,8 @@ var ReactCompositeComponentMixin = {
911928
nextState,
912929
nextContext,
913930
transaction,
914-
unmaskedContext
931+
unmaskedContext,
932+
pureSelf
915933
) {
916934
var inst = this._instance;
917935

@@ -951,7 +969,7 @@ var ReactCompositeComponentMixin = {
951969
inst.state = nextState;
952970
inst.context = nextContext;
953971

954-
this._updateRenderedComponent(transaction, unmaskedContext);
972+
this._updateRenderedComponent(transaction, unmaskedContext, pureSelf);
955973

956974
if (hasComponentDidUpdate) {
957975
if (__DEV__) {
@@ -974,7 +992,7 @@ var ReactCompositeComponentMixin = {
974992
* @param {ReactReconcileTransaction} transaction
975993
* @internal
976994
*/
977-
_updateRenderedComponent: function(transaction, context) {
995+
_updateRenderedComponent: function(transaction, context, pureSelf) {
978996
var prevComponentInstance = this._renderedComponent;
979997
var prevRenderedElement = prevComponentInstance._currentElement;
980998
var nextRenderedElement = this._renderValidatedComponent();
@@ -983,7 +1001,8 @@ var ReactCompositeComponentMixin = {
9831001
prevComponentInstance,
9841002
nextRenderedElement,
9851003
transaction,
986-
this._processChildContext(context)
1004+
this._processChildContext(context),
1005+
pureSelf
9871006
);
9881007
} else {
9891008
var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);

0 commit comments

Comments
 (0)