diff --git a/src/browser/ui/React.js b/src/browser/ui/React.js index c6125cde39f..5cbc45fa96a 100644 --- a/src/browser/ui/React.js +++ b/src/browser/ui/React.js @@ -17,6 +17,7 @@ var DOMPropertyOperations = require('DOMPropertyOperations'); var EventPluginUtils = require('EventPluginUtils'); var ReactChildren = require('ReactChildren'); var ReactComponent = require('ReactComponent'); +var ReactComponentBase = require('ReactComponentBase'); var ReactClass = require('ReactClass'); var ReactContext = require('ReactContext'); var ReactCurrentOwner = require('ReactCurrentOwner'); @@ -57,6 +58,7 @@ var React = { count: ReactChildren.count, only: onlyChild }, + Component: ReactComponentBase, DOM: ReactDOM, PropTypes: ReactPropTypes, initializeTouchEvents: function(shouldUseTouch) { diff --git a/src/classic/class/ReactClass.js b/src/classic/class/ReactClass.js index c9dc4203f7f..f5340e2d083 100644 --- a/src/classic/class/ReactClass.js +++ b/src/classic/class/ReactClass.js @@ -11,6 +11,7 @@ "use strict"; +var ReactComponentBase = require('ReactComponentBase'); var ReactElement = require('ReactElement'); var ReactErrorUtils = require('ReactErrorUtils'); var ReactInstanceMap = require('ReactInstanceMap'); @@ -22,7 +23,6 @@ var invariant = require('invariant'); var keyMirror = require('keyMirror'); var keyOf = require('keyOf'); var monitorCodeUse = require('monitorCodeUse'); -var warning = require('warning'); var MIXINS_KEY = keyOf({mixins: null}); @@ -692,49 +692,11 @@ function bindAutoBindMethods(component) { } /** - * @lends {ReactClass.prototype} + * Add more to the ReactClass base class. These are all legacy features and + * therefore not already part of the modern ReactComponentBase. */ var ReactClassMixin = { - /** - * Sets a subset of the state. Always use this or `replaceState` to mutate - * state. You should treat `this.state` as immutable. - * - * There is no guarantee that `this.state` will be immediately updated, so - * accessing `this.state` after calling this method may return the old value. - * - * There is no guarantee that calls to `setState` will run synchronously, - * as they may eventually be batched together. You can provide an optional - * callback that will be executed when the call to setState is actually - * completed. - * - * @param {object} partialState Next partial state to be merged with state. - * @param {?function} callback Called after state is updated. - * @final - * @protected - */ - setState: function(partialState, callback) { - invariant( - typeof partialState === 'object' || partialState == null, - 'setState(...): takes an object of state variables to update.' - ); - if (__DEV__) { - warning( - partialState != null, - 'setState(...): You passed an undefined or null state object; ' + - 'instead, use forceUpdate().' - ); - } - var internalInstance = ReactInstanceMap.get(this); - invariant( - internalInstance, - 'setState(...): Can only update a mounted or mounting component.' - ); - internalInstance.setState( - partialState, callback && callback.bind(this) - ); - }, - /** * TODO: This will be deprecated because state should always keep a consistent * type signature and the only use case for this, is to avoid that. @@ -743,7 +705,8 @@ var ReactClassMixin = { var internalInstance = ReactInstanceMap.get(this); invariant( internalInstance, - 'replaceState(...): Can only update a mounted or mounting component.' + 'replaceState(...): Can only update a mounted or mounting component. ' + + 'This usually means you called replaceState() on an unmounted component.' ); internalInstance.replaceState( newState, @@ -751,30 +714,6 @@ var ReactClassMixin = { ); }, - /** - * Forces an update. This should only be invoked when it is known with - * certainty that we are **not** in a DOM transaction. - * - * You may want to call this when you know that some deeper aspect of the - * component's state has changed but `setState` was not called. - * - * This will not invoke `shouldUpdateComponent`, but it will invoke - * `componentWillUpdate` and `componentDidUpdate`. - * - * @param {?function} callback Called after update is complete. - * @final - * @protected - */ - forceUpdate: function(callback) { - var internalInstance = ReactInstanceMap.get(this); - invariant( - internalInstance, - 'forceUpdate(...): Can only force an update on mounted or mounting ' + - 'components.' - ); - internalInstance.forceUpdate(callback && callback.bind(this)); - }, - /** * Checks whether or not this composite component is mounted. * @return {boolean} True if mounted, false otherwise. @@ -829,6 +768,7 @@ var ReactClassMixin = { var ReactClassBase = function() {}; assign( ReactClassBase.prototype, + ReactComponentBase.prototype, ReactClassMixin ); diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js index 101f2218e61..5122601b109 100644 --- a/src/core/__tests__/ReactCompositeComponent-test.js +++ b/src/core/__tests__/ReactCompositeComponent-test.js @@ -297,7 +297,8 @@ describe('ReactCompositeComponent', function() { instance.forceUpdate(); }).toThrow( 'Invariant Violation: forceUpdate(...): Can only force an update on ' + - 'mounted or mounting components.' + 'mounted or mounting components. This usually means you called ' + + 'forceUpdate() on an unmounted component.' ); }); @@ -327,10 +328,39 @@ describe('ReactCompositeComponent', function() { instance.setState({ value: 2 }); }).toThrow( 'Invariant Violation: setState(...): Can only update a mounted or ' + - 'mounting component.' + 'mounting component. This usually means you called setState() on an ' + + 'unmounted component.' ); }); + it('should not allow `setState` on unmounting components', function() { + var container = document.createElement('div'); + document.documentElement.appendChild(container); + + var Component = React.createClass({ + getInitialState: function() { + return { value: 0 }; + }, + componentWillUnmount: function() { + expect(() => this.setState({ value: 2 })).toThrow( + 'Invariant Violation: replaceState(...): Cannot update while ' + + 'unmounting component. This usually means you called setState() ' + + 'on an unmounted component.' + ); + }, + render: function() { + return
; + } + }); + + var instance = React.render(, container); + expect(function() { + instance.setState({ value: 1 }); + }).not.toThrow(); + + React.unmountComponentAtNode(container); + }); + it('should not allow `setProps` on unmounted components', function() { var container = document.createElement('div'); document.documentElement.appendChild(container); @@ -623,6 +653,31 @@ describe('ReactCompositeComponent', function() { ); }); + it('only renders once if updated in componentWillReceiveProps', function() { + var renders = 0; + var Component = React.createClass({ + getInitialState: function() { + return { updated: false }; + }, + componentWillReceiveProps: function(props) { + expect(props.update).toBe(1); + this.setState({ updated: true }); + }, + render: function() { + renders++; + return
; + } + }); + + var container = document.createElement('div'); + var instance = React.render(, container); + expect(renders).toBe(1); + expect(instance.state.updated).toBe(false); + React.render(, container); + expect(renders).toBe(2); + expect(instance.state.updated).toBe(true); + }); + it('should update refs if shouldComponentUpdate gives false', function() { var Static = React.createClass({ shouldComponentUpdate: function() { @@ -659,4 +714,25 @@ describe('ReactCompositeComponent', function() { expect(comp.refs.static1.getDOMNode().textContent).toBe('A'); }); + it('should allow access to getDOMNode in componentWillUnmount', function() { + var a = null; + var b = null; + var Component = React.createClass({ + componentDidMount: function() { + a = this.getDOMNode(); + }, + componentWillUnmount: function() { + b = this.getDOMNode(); + }, + render: function() { + return
; + } + }); + var container = document.createElement('div'); + expect(a).toBe(container.firstChild); + React.render(, container); + React.unmountComponentAtNode(container); + expect(a).toBe(b); + }); + }); diff --git a/src/modern/class/ReactComponentBase.js b/src/modern/class/ReactComponentBase.js new file mode 100644 index 00000000000..719adb6768e --- /dev/null +++ b/src/modern/class/ReactComponentBase.js @@ -0,0 +1,127 @@ +/** + * Copyright 2013-2014, 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 ReactComponentBase + */ + +"use strict"; + +var ReactInstanceMap = require('ReactInstanceMap'); + +var assign = require('Object.assign'); +var invariant = require('invariant'); +var warning = require('warning'); + +/** + * Base class helpers for the updating state of a component. + */ +function ReactComponentBase(props) { + this.props = props; +} + +/** + * Sets a subset of the state. Always use this to mutate + * state. You should treat `this.state` as immutable. + * + * There is no guarantee that `this.state` will be immediately updated, so + * accessing `this.state` after calling this method may return the old value. + * + * There is no guarantee that calls to `setState` will run synchronously, + * as they may eventually be batched together. You can provide an optional + * callback that will be executed when the call to setState is actually + * completed. + * + * @param {object} partialState Next partial state to be merged with state. + * @param {?function} callback Called after state is updated. + * @final + * @protected + */ +ReactComponentBase.prototype.setState = function(partialState, callback) { + invariant( + typeof partialState === 'object' || partialState == null, + 'setState(...): takes an object of state variables to update.' + ); + if (__DEV__) { + warning( + partialState != null, + 'setState(...): You passed an undefined or null state object; ' + + 'instead, use forceUpdate().' + ); + } + + var internalInstance = ReactInstanceMap.get(this); + invariant( + internalInstance, + 'setState(...): Can only update a mounted or mounting component. ' + + 'This usually means you called setState() on an unmounted ' + + 'component.' + ); + internalInstance.setState( + partialState, callback && callback.bind(this) + ); +}; + +/** + * Forces an update. This should only be invoked when it is known with + * certainty that we are **not** in a DOM transaction. + * + * You may want to call this when you know that some deeper aspect of the + * component's state has changed but `setState` was not called. + * + * This will not invoke `shouldUpdateComponent`, but it will invoke + * `componentWillUpdate` and `componentDidUpdate`. + * + * @param {?function} callback Called after update is complete. + * @final + * @protected + */ +ReactComponentBase.prototype.forceUpdate = function(callback) { + var internalInstance = ReactInstanceMap.get(this); + invariant( + internalInstance, + 'forceUpdate(...): Can only force an update on mounted or mounting ' + + 'components. This usually means you called forceUpdate() on an ' + + 'unmounted component.' + ); + internalInstance.forceUpdate(callback && callback.bind(this)); +}; + +/** + * Deprecated APIs. These APIs used to exist on classic React classes but since + * we would like to deprecate them, we're not going to move them over to this + * modern base class. Instead, we define a getter that warns if it's accessed. + */ +if (__DEV__) { + if (Object.defineProperty) { + var deprecatedAPIs = { + getDOMNode: 'getDOMNode', + isMounted: 'isMounted', + replaceState: 'replaceState', + setProps: 'setProps' + }; + var defineDeprecationWarning = function(methodName, displayName) { + Object.defineProperty(ReactComponentBase.prototype, methodName, { + get: function() { + warning( + false, + '%s(...) is deprecated in plain JavaScript React classes.', + displayName + ); + return undefined; + } + }); + }; + for (var methodName in deprecatedAPIs) { + if (deprecatedAPIs.hasOwnProperty(methodName)) { + defineDeprecationWarning(methodName, deprecatedAPIs[methodName]); + } + } + } +} + +module.exports = ReactComponentBase; diff --git a/src/modern/class/__tests__/ReactES6Class-test.js b/src/modern/class/__tests__/ReactES6Class-test.js new file mode 100644 index 00000000000..1a78f397d54 --- /dev/null +++ b/src/modern/class/__tests__/ReactES6Class-test.js @@ -0,0 +1,150 @@ +/** + * Copyright 2013-2014, 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 mocks = require('mocks'); + +var React; + +describe('ReactES6Class', function() { + + var container; + var Inner; + var attachedListener = null; + var renderedName = null; + + beforeEach(function() { + React = require('React'); + container = document.createElement(); + attachedListener = null; + renderedName = null; + Inner = class extends React.Component { + getName() { + return this.props.name; + } + render() { + attachedListener = this.props.onClick; + renderedName = this.props.name; + return
; + } + }; + }); + + function test(element, expectedTag, expectedClassName) { + var instance = React.render(element, container); + expect(container.firstChild).not.toBeNull(); + expect(container.firstChild.tagName).toBe(expectedTag); + expect(container.firstChild.className).toBe(expectedClassName); + return instance; + } + + it('preserves the name of the class for use in error messages', function() { + class Foo extends React.Component { } + expect(Foo.name).toBe('Foo'); + }); + + it('throws if no render function is defined', function() { + class Foo extends React.Component { } + expect(() => React.render(, container)).toThrow(); + }); + + it('renders a simple stateless component with prop', function() { + class Foo { + render() { + return ; + } + } + test(, 'DIV', 'foo'); + test(, 'DIV', 'bar'); + }); + + it('renders using forceUpdate even when there is no state', function() { + class Foo extends React.Component { + constructor(props) { + this.mutativeValue = props.initialValue; + } + handleClick() { + this.mutativeValue = 'bar'; + this.forceUpdate(); + } + render() { + return ( + + ); + } + } + test(, 'DIV', 'foo'); + attachedListener(); + expect(renderedName).toBe('bar'); + }); + + it('should throw AND warn when trying to access classic APIs', function() { + spyOn(console, 'warn'); + var instance = test(, 'DIV', 'foo'); + expect(() => instance.getDOMNode()).toThrow(); + expect(() => instance.replaceState({})).toThrow(); + expect(() => instance.isMounted()).toThrow(); + expect(() => instance.setProps({ name: 'bar' })).toThrow(); + expect(console.warn.calls.length).toBe(4); + expect(console.warn.calls[0].args[0]).toContain( + 'getDOMNode(...) is deprecated in plain JavaScript React classes' + ); + expect(console.warn.calls[1].args[0]).toContain( + 'replaceState(...) is deprecated in plain JavaScript React classes' + ); + expect(console.warn.calls[2].args[0]).toContain( + 'isMounted(...) is deprecated in plain JavaScript React classes' + ); + expect(console.warn.calls[3].args[0]).toContain( + 'setProps(...) is deprecated in plain JavaScript React classes' + ); + }); + + it('supports this.context passed via getChildContext', function() { + class Bar { + render() { + return
; + } + } + Bar.contextTypes = { bar: React.PropTypes.string }; + class Foo { + getChildContext() { + return { bar: 'bar-through-context' }; + } + render() { + return ; + } + } + Foo.childContextTypes = { bar: React.PropTypes.string }; + test(, 'DIV', 'bar-through-context'); + }); + + it('supports classic refs', function() { + class Foo { + render() { + return ; + } + } + var instance = test(, 'DIV', 'foo'); + expect(instance.refs.inner.getName()).toBe('foo'); + }); + + it('supports drilling through to the DOM using findDOMNode', function() { + var instance = test(, 'DIV', 'foo'); + var node = React.findDOMNode(instance); + expect(node).toBe(container.firstChild); + }); + +});