Add React.PureComponent (#7195)
This provides an easy way to indicate that components should only rerender when given new props, like PureRenderMixin. If you rely on mutation in your React components, you can continue to use `React.Component`. Inheriting from `React.PureComponent` indicates to React that your component doesn't need to rerender when the props are unchanged. We'll compare the old and new props before each render and short-circuit if they're unchanged. It's like an automatic shouldComponentUpdate.
Ben Alpert committed
Jul 6, 2016 at 13:24 UTC
c8fbdac22717e91d1f1638a932eed3ce4cc39ff7
4 files changed
+219
-28
src/isomorphic/React.js
+2
@@ -13,6 +13,7 @@
13
14
var ReactChildren = require('ReactChildren');
15
var ReactComponent = require('ReactComponent');
16
+var ReactPureComponent = require('ReactPureComponent');
17
var ReactClass = require('ReactClass');
18
var ReactDOMFactories = require('ReactDOMFactories');
19
var ReactElement = require('ReactElement');
@@ -63,6 +64,7 @@ var React = {
64
},
65
66
Component: ReactComponent,
67
+ PureComponent: ReactPureComponent,
68
69
createElement: createElement,
70
cloneElement: cloneElement,
src/isomorphic/modern/class/ReactPureComponent.js
new
+40
@@ -0,0 +1,40 @@
1
+/**
2
+ * Copyright 2013-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @providesModule ReactPureComponent
10
+ */
11
+
12
+'use strict';
13
+
14
+var ReactComponent = require('ReactComponent');
15
+var ReactNoopUpdateQueue = require('ReactNoopUpdateQueue');
16
+
17
+var emptyObject = require('emptyObject');
18
+
19
+/**
20
+ * Base class helpers for the updating state of a component.
21
+ */
22
+function ReactPureComponent(props, context, updater) {
23
+ // Duplicated from ReactComponent.
24
+ this.props = props;
25
+ this.context = context;
26
+ this.refs = emptyObject;
27
+ // We initialize the default updater but the real one gets injected by the
28
+ // renderer.
29
+ this.updater = updater || ReactNoopUpdateQueue;
30
+}
31
+
32
+function ComponentDummy() {}
33
+ComponentDummy.prototype = ReactComponent.prototype;
34
+ReactPureComponent.prototype = new ComponentDummy();
35
+ReactPureComponent.prototype.constructor = ReactPureComponent;
36
+// Avoid an extra prototype jump for these methods.
37
+Object.assign(ReactPureComponent.prototype, ReactComponent.prototype);
38
+ReactPureComponent.prototype.isPureReactComponent = true;
39
+
40
+module.exports = ReactPureComponent;
src/isomorphic/modern/class/__tests__/ReactPureComponent-test.js
new
+97
@@ -0,0 +1,97 @@
1
+/**
2
+ * Copyright 2013-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @emails react-core
10
+ */
11
+
12
+'use strict';
13
+
14
+var React;
15
+var ReactDOM;
16
+
17
+describe('ReactPureComponent', function() {
18
+ beforeEach(function() {
19
+ React = require('React');
20
+ ReactDOM = require('ReactDOM');
21
+ });
22
+
23
+ it('should render', function() {
24
+ var renders = 0;
25
+ class Component extends React.PureComponent {
26
+ constructor() {
27
+ super();
28
+ this.state = {type: 'mushrooms'};
29
+ }
30
+ render() {
31
+ renders++;
32
+ return <div>{this.props.text[0]}</div>;
33
+ }
34
+ }
35
+
36
+ var container = document.createElement('div');
37
+ var text;
38
+ var component;
39
+
40
+ text = ['porcini'];
41
+ component = ReactDOM.render(<Component text={text} />, container);
42
+ expect(container.textContent).toBe('porcini');
43
+ expect(renders).toBe(1);
44
+
45
+ text = ['morel'];
46
+ component = ReactDOM.render(<Component text={text} />, container);
47
+ expect(container.textContent).toBe('morel');
48
+ expect(renders).toBe(2);
49
+
50
+ text[0] = 'portobello';
51
+ component = ReactDOM.render(<Component text={text} />, container);
52
+ expect(container.textContent).toBe('morel');
53
+ expect(renders).toBe(2);
54
+
55
+ // Setting state without changing it doesn't cause a rerender.
56
+ component.setState({type: 'mushrooms'});
57
+ expect(container.textContent).toBe('morel');
58
+ expect(renders).toBe(2);
59
+
60
+ // But changing state does.
61
+ component.setState({type: 'portobello mushrooms'});
62
+ expect(container.textContent).toBe('portobello');
63
+ expect(renders).toBe(3);
64
+ });
65
+
66
+ it('can override shouldComponentUpdate', function() {
67
+ var renders = 0;
68
+ class Component extends React.PureComponent {
69
+ render() {
70
+ renders++;
71
+ return <div />;
72
+ }
73
+ shouldComponentUpdate() {
74
+ return true;
75
+ }
76
+ }
77
+ var container = document.createElement('div');
78
+ ReactDOM.render(<Component />, container);
79
+ ReactDOM.render(<Component />, container);
80
+ expect(renders).toBe(2);
81
+ });
82
+
83
+ it('extends React.Component', function() {
84
+ var renders = 0;
85
+ class Component extends React.PureComponent {
86
+ render() {
87
+ expect(this instanceof React.Component).toBe(true);
88
+ expect(this instanceof React.PureComponent).toBe(true);
89
+ renders++;
90
+ return <div />;
91
+ }
92
+ }
93
+ ReactDOM.render(<Component />, document.createElement('div'));
94
+ expect(renders).toBe(1);
95
+ });
96
+
97
+});
src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
+80
-28
@@ -22,12 +22,18 @@ var ReactPropTypeLocations = require('ReactPropTypeLocations');
22
var ReactReconciler = require('ReactReconciler');
23
24
var checkReactTypeSpec = require('checkReactTypeSpec');
25
-
25
var emptyObject = require('emptyObject');
26
var invariant = require('invariant');
27
+var shallowEqual = require('shallowEqual');
28
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
29
var warning = require('warning');
30
31
+var CompositeTypes = {
32
+ ImpureClass: 0,
33
+ PureClass: 1,
34
+ StatelessFunctional: 2,
35
+};
36
+
37
function StatelessComponent(Component) {
38
}
39
StatelessComponent.prototype.render = function() {
@@ -88,7 +94,11 @@ function invokeComponentDidUpdateWithTimer(prevProps, prevState, prevContext) {
94
}
95
96
function shouldConstruct(Component) {
91
- return Component.prototype && Component.prototype.isReactComponent;
97
+ return !!(Component.prototype && Component.prototype.isReactComponent);
98
+}
99
+
100
+function isPureComponent(Component) {
101
+ return !!(Component.prototype && Component.prototype.isPureReactComponent);
102
}
103
104
/**
@@ -141,6 +151,7 @@ var ReactCompositeComponentMixin = {
151
construct: function(element) {
152
this._currentElement = element;
153
this._rootNodeID = null;
154
+ this._compositeType = null;
155
this._instance = null;
156
this._hostParent = null;
157
this._hostContainerInfo = null;
@@ -199,11 +210,17 @@ var ReactCompositeComponentMixin = {
210
var updateQueue = transaction.getUpdateQueue();
211
212
// Initialize the public class
202
- var inst = this._constructComponent(publicProps, publicContext, updateQueue);
213
+ var doConstruct = shouldConstruct(Component);
214
+ var inst = this._constructComponent(
215
+ doConstruct,
216
+ publicProps,
217
+ publicContext,
218
+ updateQueue
219
+ );
220
var renderedElement;
221
222
// Support functional components
206
- if (!shouldConstruct(Component) && (inst == null || inst.render == null)) {
223
+ if (!doConstruct && (inst == null || inst.render == null)) {
224
renderedElement = inst;
225
warnIfInvalidElement(Component, renderedElement);
226
invariant(
@@ -215,6 +232,13 @@ var ReactCompositeComponentMixin = {
232
Component.displayName || Component.name || 'Component'
233
);
234
inst = new StatelessComponent(Component);
235
+ this._compositeType = CompositeTypes.StatelessFunctional;
236
+ } else {
237
+ if (isPureComponent(Component)) {
238
+ this._compositeType = CompositeTypes.PureClass;
239
+ } else {
240
+ this._compositeType = CompositeTypes.ImpureClass;
241
+ }
242
}
243
244
if (__DEV__) {
@@ -353,23 +377,43 @@ var ReactCompositeComponentMixin = {
377
return markup;
378
},
379
356
- _constructComponent: function(publicProps, publicContext, updateQueue) {
380
+ _constructComponent: function(
381
+ doConstruct,
382
+ publicProps,
383
+ publicContext,
384
+ updateQueue
385
+ ) {
386
if (__DEV__) {
387
ReactCurrentOwner.current = this;
388
try {
360
- return this._constructComponentWithoutOwner(publicProps, publicContext, updateQueue);
389
+ return this._constructComponentWithoutOwner(
390
+ doConstruct,
391
+ publicProps,
392
+ publicContext,
393
+ updateQueue
394
+ );
395
} finally {
396
ReactCurrentOwner.current = null;
397
}
398
} else {
365
- return this._constructComponentWithoutOwner(publicProps, publicContext, updateQueue);
399
+ return this._constructComponentWithoutOwner(
400
+ doConstruct,
401
+ publicProps,
402
+ publicContext,
403
+ updateQueue
404
+ );
405
}
406
},
407
369
- _constructComponentWithoutOwner: function(publicProps, publicContext, updateQueue) {
408
+ _constructComponentWithoutOwner: function(
409
+ doConstruct,
410
+ publicProps,
411
+ publicContext,
412
+ updateQueue
413
+ ) {
414
var Component = this._currentElement.type;
415
var instanceOrElement;
372
- if (shouldConstruct(Component)) {
416
+ if (doConstruct) {
417
if (__DEV__) {
418
if (this._debugID !== 0) {
419
ReactInstrumentation.debugTool.onBeginLifeCycleTimer(
@@ -765,7 +809,6 @@ var ReactCompositeComponentMixin = {
809
810
var willReceive = false;
811
var nextContext;
768
- var nextProps;
812
813
// Determine if the context has changed or not
814
if (this._context === nextUnmaskedContext) {
@@ -775,7 +818,8 @@ var ReactCompositeComponentMixin = {
818
willReceive = true;
819
}
820
778
- nextProps = nextParentElement.props;
821
+ var prevProps = prevParentElement.props;
822
+ var nextProps = nextParentElement.props;
823
824
// Not a simple state update but a props update
825
if (prevParentElement !== nextParentElement) {
@@ -808,22 +852,30 @@ var ReactCompositeComponentMixin = {
852
var nextState = this._processPendingState(nextProps, nextContext);
853
var shouldUpdate = true;
854
811
- if (!this._pendingForceUpdate && inst.shouldComponentUpdate) {
812
- if (__DEV__) {
813
- if (this._debugID !== 0) {
814
- ReactInstrumentation.debugTool.onBeginLifeCycleTimer(
815
- this._debugID,
816
- 'shouldComponentUpdate'
817
- );
855
+ if (!this._pendingForceUpdate) {
856
+ if (inst.shouldComponentUpdate) {
857
+ if (__DEV__) {
858
+ if (this._debugID !== 0) {
859
+ ReactInstrumentation.debugTool.onBeginLifeCycleTimer(
860
+ this._debugID,
861
+ 'shouldComponentUpdate'
862
+ );
863
+ }
864
}
819
- }
820
- shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
821
- if (__DEV__) {
822
- if (this._debugID !== 0) {
823
- ReactInstrumentation.debugTool.onEndLifeCycleTimer(
824
- this._debugID,
825
- 'shouldComponentUpdate'
826
- );
865
+ shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
866
+ if (__DEV__) {
867
+ if (this._debugID !== 0) {
868
+ ReactInstrumentation.debugTool.onEndLifeCycleTimer(
869
+ this._debugID,
870
+ 'shouldComponentUpdate'
871
+ );
872
+ }
873
+ }
874
+ } else {
875
+ if (this._compositeType === CompositeTypes.PureClass) {
876
+ shouldUpdate =
877
+ !shallowEqual(prevProps, nextProps) ||
878
+ !shallowEqual(inst.state, nextState);
879
}
880
}
881
}
@@ -1088,7 +1140,7 @@ var ReactCompositeComponentMixin = {
1140
*/
1141
_renderValidatedComponent: function() {
1142
var renderedComponent;
1091
- if (__DEV__ || !(this._instance instanceof StatelessComponent)) {
1143
+ if (__DEV__ || this._compositeType !== CompositeTypes.StatelessFunctional) {
1144
ReactCurrentOwner.current = this;
1145
try {
1146
renderedComponent =
@@ -1178,7 +1230,7 @@ var ReactCompositeComponentMixin = {
1230
*/
1231
getPublicInstance: function() {
1232
var inst = this._instance;
1181
- if (inst instanceof StatelessComponent) {
1233
+ if (this._compositeType === CompositeTypes.StatelessFunctional) {
1234
return null;
1235
}
1236
return inst;