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);
+ });
+
+});