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;