diff --git a/src/isomorphic/React.js b/src/isomorphic/React.js
index 92fa89cad3ba1..c791b30e08cf0 100644
--- a/src/isomorphic/React.js
+++ b/src/isomorphic/React.js
@@ -13,6 +13,7 @@
var ReactChildren = require('ReactChildren');
var ReactComponent = require('ReactComponent');
+var ReactPureComponent = require('ReactPureComponent');
var ReactClass = require('ReactClass');
var ReactDOMFactories = require('ReactDOMFactories');
var ReactElement = require('ReactElement');
@@ -63,6 +64,7 @@ var React = {
},
Component: ReactComponent,
+ PureComponent: ReactPureComponent,
createElement: createElement,
cloneElement: cloneElement,
diff --git a/src/isomorphic/modern/class/ReactPureComponent.js b/src/isomorphic/modern/class/ReactPureComponent.js
new file mode 100644
index 0000000000000..be0be62c211a2
--- /dev/null
+++ b/src/isomorphic/modern/class/ReactPureComponent.js
@@ -0,0 +1,26 @@
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactPureComponent
+ */
+
+'use strict';
+
+var ReactComponent = require('ReactComponent');
+
+/**
+ * Base class helpers for the updating state of a component.
+ */
+function ReactPureComponent(props, context, updater) {
+ ReactComponent.call(this, props, context, updater);
+}
+
+Object.assign(ReactPureComponent.prototype, ReactComponent.prototype);
+ReactPureComponent.prototype.isPureReactComponent = true;
+
+module.exports = ReactPureComponent;
diff --git a/src/isomorphic/modern/class/__tests__/ReactPureComponent-test.js b/src/isomorphic/modern/class/__tests__/ReactPureComponent-test.js
new file mode 100644
index 0000000000000..2619df7abf125
--- /dev/null
+++ b/src/isomorphic/modern/class/__tests__/ReactPureComponent-test.js
@@ -0,0 +1,83 @@
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+var React;
+var ReactDOM;
+
+describe('ReactPureComponent', function() {
+ beforeEach(function() {
+ React = require('React');
+ ReactDOM = require('ReactDOM');
+ });
+
+ it('should render', function() {
+ var renders = 0;
+ class Component extends React.PureComponent {
+ constructor() {
+ super();
+ this.state = {type: 'mushrooms'};
+ }
+ render() {
+ renders++;
+ return
{this.props.text[0]}
;
+ }
+ }
+
+ var container = document.createElement('div');
+ var text;
+ var component;
+
+ text = ['porcini'];
+ component = ReactDOM.render(, container);
+ expect(container.textContent).toBe('porcini');
+ expect(renders).toBe(1);
+
+ text = ['morel'];
+ component = ReactDOM.render(, container);
+ expect(container.textContent).toBe('morel');
+ expect(renders).toBe(2);
+
+ text[0] = 'portobello';
+ component = ReactDOM.render(, container);
+ expect(container.textContent).toBe('morel');
+ expect(renders).toBe(2);
+
+ // Setting state without changing it doesn't cause a rerender.
+ component.setState({type: 'mushrooms'});
+ expect(container.textContent).toBe('morel');
+ expect(renders).toBe(2);
+
+ // But changing state does.
+ component.setState({type: 'portobello mushrooms'});
+ expect(container.textContent).toBe('portobello');
+ expect(renders).toBe(3);
+ });
+
+ it('can override shouldComponentUpdate', function() {
+ var renders = 0;
+ class Component extends React.PureComponent {
+ render() {
+ renders++;
+ return ;
+ }
+ shouldComponentUpdate() {
+ return true;
+ }
+ }
+ var container = document.createElement('div');
+ ReactDOM.render(, container);
+ ReactDOM.render(, container);
+ expect(renders).toBe(2);
+ });
+
+});
diff --git a/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js b/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
index 142598ecd4361..f80ad75b80f61 100644
--- a/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
+++ b/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
@@ -22,12 +22,18 @@ var ReactPropTypeLocations = require('ReactPropTypeLocations');
var ReactReconciler = require('ReactReconciler');
var checkReactTypeSpec = require('checkReactTypeSpec');
-
var emptyObject = require('emptyObject');
var invariant = require('invariant');
+var shallowEqual = require('shallowEqual');
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
var warning = require('warning');
+var CompositeTypes = {
+ ImpureClass: 0,
+ PureClass: 1,
+ StatelessFunctional: 2,
+};
+
function StatelessComponent(Component) {
}
StatelessComponent.prototype.render = function() {
@@ -88,7 +94,11 @@ function invokeComponentDidUpdateWithTimer(prevProps, prevState, prevContext) {
}
function shouldConstruct(Component) {
- return Component.prototype && Component.prototype.isReactComponent;
+ return !!(Component.prototype && Component.prototype.isReactComponent);
+}
+
+function isPureComponent(Component) {
+ return !!(Component.prototype && Component.prototype.isPureReactComponent);
}
/**
@@ -141,6 +151,7 @@ var ReactCompositeComponentMixin = {
construct: function(element) {
this._currentElement = element;
this._rootNodeID = null;
+ this._compositeType = null;
this._instance = null;
this._hostParent = null;
this._hostContainerInfo = null;
@@ -199,11 +210,17 @@ var ReactCompositeComponentMixin = {
var updateQueue = transaction.getUpdateQueue();
// Initialize the public class
- var inst = this._constructComponent(publicProps, publicContext, updateQueue);
+ var doConstruct = shouldConstruct(Component);
+ var inst = this._constructComponent(
+ doConstruct,
+ publicProps,
+ publicContext,
+ updateQueue
+ );
var renderedElement;
// Support functional components
- if (!shouldConstruct(Component) && (inst == null || inst.render == null)) {
+ if (!doConstruct && (inst == null || inst.render == null)) {
renderedElement = inst;
warnIfInvalidElement(Component, renderedElement);
invariant(
@@ -215,6 +232,13 @@ var ReactCompositeComponentMixin = {
Component.displayName || Component.name || 'Component'
);
inst = new StatelessComponent(Component);
+ this._compositeType = CompositeTypes.StatelessFunctional;
+ } else {
+ if (isPureComponent(Component)) {
+ this._compositeType = CompositeTypes.PureClass;
+ } else {
+ this._compositeType = CompositeTypes.ImpureClass;
+ }
}
if (__DEV__) {
@@ -353,23 +377,43 @@ var ReactCompositeComponentMixin = {
return markup;
},
- _constructComponent: function(publicProps, publicContext, updateQueue) {
+ _constructComponent: function(
+ doConstruct,
+ publicProps,
+ publicContext,
+ updateQueue
+ ) {
if (__DEV__) {
ReactCurrentOwner.current = this;
try {
- return this._constructComponentWithoutOwner(publicProps, publicContext, updateQueue);
+ return this._constructComponentWithoutOwner(
+ doConstruct,
+ publicProps,
+ publicContext,
+ updateQueue
+ );
} finally {
ReactCurrentOwner.current = null;
}
} else {
- return this._constructComponentWithoutOwner(publicProps, publicContext, updateQueue);
+ return this._constructComponentWithoutOwner(
+ doConstruct,
+ publicProps,
+ publicContext,
+ updateQueue
+ );
}
},
- _constructComponentWithoutOwner: function(publicProps, publicContext, updateQueue) {
+ _constructComponentWithoutOwner: function(
+ doConstruct,
+ publicProps,
+ publicContext,
+ updateQueue
+ ) {
var Component = this._currentElement.type;
var instanceOrElement;
- if (shouldConstruct(Component)) {
+ if (doConstruct) {
if (__DEV__) {
if (this._debugID !== 0) {
ReactInstrumentation.debugTool.onBeginLifeCycleTimer(
@@ -763,7 +807,6 @@ var ReactCompositeComponentMixin = {
var willReceive = false;
var nextContext;
- var nextProps;
// Determine if the context has changed or not
if (this._context === nextUnmaskedContext) {
@@ -773,7 +816,8 @@ var ReactCompositeComponentMixin = {
willReceive = true;
}
- nextProps = nextParentElement.props;
+ var prevProps = prevParentElement.props;
+ var nextProps = nextParentElement.props;
// Not a simple state update but a props update
if (prevParentElement !== nextParentElement) {
@@ -806,22 +850,30 @@ var ReactCompositeComponentMixin = {
var nextState = this._processPendingState(nextProps, nextContext);
var shouldUpdate = true;
- if (!this._pendingForceUpdate && inst.shouldComponentUpdate) {
- if (__DEV__) {
- if (this._debugID !== 0) {
- ReactInstrumentation.debugTool.onBeginLifeCycleTimer(
- this._debugID,
- 'shouldComponentUpdate'
- );
+ 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 = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
+ if (__DEV__) {
+ if (this._debugID !== 0) {
+ ReactInstrumentation.debugTool.onEndLifeCycleTimer(
+ this._debugID,
+ 'shouldComponentUpdate'
+ );
+ }
+ }
+ } else {
+ if (this._compositeType === CompositeTypes.PureClass) {
+ shouldUpdate =
+ !shallowEqual(prevProps, nextProps) ||
+ !shallowEqual(inst.state, nextState);
}
}
}
@@ -1084,7 +1136,7 @@ var ReactCompositeComponentMixin = {
*/
_renderValidatedComponent: function() {
var renderedComponent;
- if (__DEV__ || !(this._instance instanceof StatelessComponent)) {
+ if (__DEV__ || this._compositeType !== CompositeTypes.StatelessFunctional) {
ReactCurrentOwner.current = this;
try {
renderedComponent =
@@ -1174,7 +1226,7 @@ var ReactCompositeComponentMixin = {
*/
getPublicInstance: function() {
var inst = this._instance;
- if (inst instanceof StatelessComponent) {
+ if (this._compositeType === CompositeTypes.StatelessFunctional) {
return null;
}
return inst;