Introducing ReactComponentBase base class
This is the base class that will be used by ES6 classes. I'm only moving setState and forceUpdate to this base class and the other functions are disabled for modern classes as we're intending to deprecate them. The base classes only have getters that warn if accessed. It's as if they didn't exist. ReactClass now extends ReactComponentBase but also adds the deprecated methods. They are not yet fully deprecated on the ReactClass API. I added some extra tests to composite component which we weren't testing to avoid regressions. I also added some test for ES6 classes. These are not testing the new state initialization process. That's coming in a follow up.
Sebastian Markbage committed
Jan 4, 2015 at 18:21 UTC
ff032dc8576fec0bf6dbeeaf5d53325c2010d284
5 files changed
+363
-68
src/browser/ui/React.js
+2
@@ -17,6 +17,7 @@ var DOMPropertyOperations = require('DOMPropertyOperations');
17
var EventPluginUtils = require('EventPluginUtils');
18
var ReactChildren = require('ReactChildren');
19
var ReactComponent = require('ReactComponent');
20
+var ReactComponentBase = require('ReactComponentBase');
21
var ReactClass = require('ReactClass');
22
var ReactContext = require('ReactContext');
23
var ReactCurrentOwner = require('ReactCurrentOwner');
@@ -57,6 +58,7 @@ var React = {
58
count: ReactChildren.count,
59
only: onlyChild
60
},
61
+ Component: ReactComponentBase,
62
DOM: ReactDOM,
63
PropTypes: ReactPropTypes,
64
initializeTouchEvents: function(shouldUseTouch) {
src/classic/class/ReactClass.js
+6
-66
@@ -11,6 +11,7 @@
11
12
"use strict";
13
14
+var ReactComponentBase = require('ReactComponentBase');
15
var ReactElement = require('ReactElement');
16
var ReactErrorUtils = require('ReactErrorUtils');
17
var ReactInstanceMap = require('ReactInstanceMap');
@@ -22,7 +23,6 @@ var invariant = require('invariant');
23
var keyMirror = require('keyMirror');
24
var keyOf = require('keyOf');
25
var monitorCodeUse = require('monitorCodeUse');
25
-var warning = require('warning');
26
27
var MIXINS_KEY = keyOf({mixins: null});
28
@@ -692,49 +692,11 @@ function bindAutoBindMethods(component) {
692
}
693
694
/**
695
- * @lends {ReactClass.prototype}
695
+ * Add more to the ReactClass base class. These are all legacy features and
696
+ * therefore not already part of the modern ReactComponentBase.
697
*/
698
var ReactClassMixin = {
699
699
- /**
700
- * Sets a subset of the state. Always use this or `replaceState` to mutate
701
- * state. You should treat `this.state` as immutable.
702
- *
703
- * There is no guarantee that `this.state` will be immediately updated, so
704
- * accessing `this.state` after calling this method may return the old value.
705
- *
706
- * There is no guarantee that calls to `setState` will run synchronously,
707
- * as they may eventually be batched together. You can provide an optional
708
- * callback that will be executed when the call to setState is actually
709
- * completed.
710
- *
711
- * @param {object} partialState Next partial state to be merged with state.
712
- * @param {?function} callback Called after state is updated.
713
- * @final
714
- * @protected
715
- */
716
- setState: function(partialState, callback) {
717
- invariant(
718
- typeof partialState === 'object' || partialState == null,
719
- 'setState(...): takes an object of state variables to update.'
720
- );
721
- if (__DEV__) {
722
- warning(
723
- partialState != null,
724
- 'setState(...): You passed an undefined or null state object; ' +
725
- 'instead, use forceUpdate().'
726
- );
727
- }
728
- var internalInstance = ReactInstanceMap.get(this);
729
- invariant(
730
- internalInstance,
731
- 'setState(...): Can only update a mounted or mounting component.'
732
- );
733
- internalInstance.setState(
734
- partialState, callback && callback.bind(this)
735
- );
736
- },
737
-
700
/**
701
* TODO: This will be deprecated because state should always keep a consistent
702
* type signature and the only use case for this, is to avoid that.
@@ -743,7 +705,8 @@ var ReactClassMixin = {
705
var internalInstance = ReactInstanceMap.get(this);
706
invariant(
707
internalInstance,
746
- 'replaceState(...): Can only update a mounted or mounting component.'
708
+ 'replaceState(...): Can only update a mounted or mounting component. ' +
709
+ 'This usually means you called replaceState() on an unmounted component.'
710
);
711
internalInstance.replaceState(
712
newState,
@@ -751,30 +714,6 @@ var ReactClassMixin = {
714
);
715
},
716
754
- /**
755
- * Forces an update. This should only be invoked when it is known with
756
- * certainty that we are **not** in a DOM transaction.
757
- *
758
- * You may want to call this when you know that some deeper aspect of the
759
- * component's state has changed but `setState` was not called.
760
- *
761
- * This will not invoke `shouldUpdateComponent`, but it will invoke
762
- * `componentWillUpdate` and `componentDidUpdate`.
763
- *
764
- * @param {?function} callback Called after update is complete.
765
- * @final
766
- * @protected
767
- */
768
- forceUpdate: function(callback) {
769
- var internalInstance = ReactInstanceMap.get(this);
770
- invariant(
771
- internalInstance,
772
- 'forceUpdate(...): Can only force an update on mounted or mounting ' +
773
- 'components.'
774
- );
775
- internalInstance.forceUpdate(callback && callback.bind(this));
776
- },
777
-
717
/**
718
* Checks whether or not this composite component is mounted.
719
* @return {boolean} True if mounted, false otherwise.
@@ -829,6 +768,7 @@ var ReactClassMixin = {
768
var ReactClassBase = function() {};
769
assign(
770
ReactClassBase.prototype,
771
+ ReactComponentBase.prototype,
772
ReactClassMixin
773
);
774
src/core/__tests__/ReactCompositeComponent-test.js
+78
-2
@@ -297,7 +297,8 @@ describe('ReactCompositeComponent', function() {
297
instance.forceUpdate();
298
}).toThrow(
299
'Invariant Violation: forceUpdate(...): Can only force an update on ' +
300
- 'mounted or mounting components.'
300
+ 'mounted or mounting components. This usually means you called ' +
301
+ 'forceUpdate() on an unmounted component.'
302
);
303
});
304
@@ -327,10 +328,39 @@ describe('ReactCompositeComponent', function() {
328
instance.setState({ value: 2 });
329
}).toThrow(
330
'Invariant Violation: setState(...): Can only update a mounted or ' +
330
- 'mounting component.'
331
+ 'mounting component. This usually means you called setState() on an ' +
332
+ 'unmounted component.'
333
);
334
});
335
336
+ it('should not allow `setState` on unmounting components', function() {
337
+ var container = document.createElement('div');
338
+ document.documentElement.appendChild(container);
339
+
340
+ var Component = React.createClass({
341
+ getInitialState: function() {
342
+ return { value: 0 };
343
+ },
344
+ componentWillUnmount: function() {
345
+ expect(() => this.setState({ value: 2 })).toThrow(
346
+ 'Invariant Violation: replaceState(...): Cannot update while ' +
347
+ 'unmounting component. This usually means you called setState() ' +
348
+ 'on an unmounted component.'
349
+ );
350
+ },
351
+ render: function() {
352
+ return <div />;
353
+ }
354
+ });
355
+
356
+ var instance = React.render(<Component />, container);
357
+ expect(function() {
358
+ instance.setState({ value: 1 });
359
+ }).not.toThrow();
360
+
361
+ React.unmountComponentAtNode(container);
362
+ });
363
+
364
it('should not allow `setProps` on unmounted components', function() {
365
var container = document.createElement('div');
366
document.documentElement.appendChild(container);
@@ -623,6 +653,31 @@ describe('ReactCompositeComponent', function() {
653
);
654
});
655
656
+ it('only renders once if updated in componentWillReceiveProps', function() {
657
+ var renders = 0;
658
+ var Component = React.createClass({
659
+ getInitialState: function() {
660
+ return { updated: false };
661
+ },
662
+ componentWillReceiveProps: function(props) {
663
+ expect(props.update).toBe(1);
664
+ this.setState({ updated: true });
665
+ },
666
+ render: function() {
667
+ renders++;
668
+ return <div />;
669
+ }
670
+ });
671
+
672
+ var container = document.createElement('div');
673
+ var instance = React.render(<Component update={0} />, container);
674
+ expect(renders).toBe(1);
675
+ expect(instance.state.updated).toBe(false);
676
+ React.render(<Component update={1} />, container);
677
+ expect(renders).toBe(2);
678
+ expect(instance.state.updated).toBe(true);
679
+ });
680
+
681
it('should update refs if shouldComponentUpdate gives false', function() {
682
var Static = React.createClass({
683
shouldComponentUpdate: function() {
@@ -659,4 +714,25 @@ describe('ReactCompositeComponent', function() {
714
expect(comp.refs.static1.getDOMNode().textContent).toBe('A');
715
});
716
717
+ it('should allow access to getDOMNode in componentWillUnmount', function() {
718
+ var a = null;
719
+ var b = null;
720
+ var Component = React.createClass({
721
+ componentDidMount: function() {
722
+ a = this.getDOMNode();
723
+ },
724
+ componentWillUnmount: function() {
725
+ b = this.getDOMNode();
726
+ },
727
+ render: function() {
728
+ return <div />;
729
+ }
730
+ });
731
+ var container = document.createElement('div');
732
+ expect(a).toBe(container.firstChild);
733
+ React.render(<Component />, container);
734
+ React.unmountComponentAtNode(container);
735
+ expect(a).toBe(b);
736
+ });
737
+
738
});
src/modern/class/ReactComponentBase.js
new
+127
@@ -0,0 +1,127 @@
1
+/**
2
+ * Copyright 2013-2014, 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 ReactComponentBase
10
+ */
11
+
12
+"use strict";
13
+
14
+var ReactInstanceMap = require('ReactInstanceMap');
15
+
16
+var assign = require('Object.assign');
17
+var invariant = require('invariant');
18
+var warning = require('warning');
19
+
20
+/**
21
+ * Base class helpers for the updating state of a component.
22
+ */
23
+function ReactComponentBase(props) {
24
+ this.props = props;
25
+}
26
+
27
+/**
28
+ * Sets a subset of the state. Always use this to mutate
29
+ * state. You should treat `this.state` as immutable.
30
+ *
31
+ * There is no guarantee that `this.state` will be immediately updated, so
32
+ * accessing `this.state` after calling this method may return the old value.
33
+ *
34
+ * There is no guarantee that calls to `setState` will run synchronously,
35
+ * as they may eventually be batched together. You can provide an optional
36
+ * callback that will be executed when the call to setState is actually
37
+ * completed.
38
+ *
39
+ * @param {object} partialState Next partial state to be merged with state.
40
+ * @param {?function} callback Called after state is updated.
41
+ * @final
42
+ * @protected
43
+ */
44
+ReactComponentBase.prototype.setState = function(partialState, callback) {
45
+ invariant(
46
+ typeof partialState === 'object' || partialState == null,
47
+ 'setState(...): takes an object of state variables to update.'
48
+ );
49
+ if (__DEV__) {
50
+ warning(
51
+ partialState != null,
52
+ 'setState(...): You passed an undefined or null state object; ' +
53
+ 'instead, use forceUpdate().'
54
+ );
55
+ }
56
+
57
+ var internalInstance = ReactInstanceMap.get(this);
58
+ invariant(
59
+ internalInstance,
60
+ 'setState(...): Can only update a mounted or mounting component. ' +
61
+ 'This usually means you called setState() on an unmounted ' +
62
+ 'component.'
63
+ );
64
+ internalInstance.setState(
65
+ partialState, callback && callback.bind(this)
66
+ );
67
+};
68
+
69
+/**
70
+ * Forces an update. This should only be invoked when it is known with
71
+ * certainty that we are **not** in a DOM transaction.
72
+ *
73
+ * You may want to call this when you know that some deeper aspect of the
74
+ * component's state has changed but `setState` was not called.
75
+ *
76
+ * This will not invoke `shouldUpdateComponent`, but it will invoke
77
+ * `componentWillUpdate` and `componentDidUpdate`.
78
+ *
79
+ * @param {?function} callback Called after update is complete.
80
+ * @final
81
+ * @protected
82
+ */
83
+ReactComponentBase.prototype.forceUpdate = function(callback) {
84
+ var internalInstance = ReactInstanceMap.get(this);
85
+ invariant(
86
+ internalInstance,
87
+ 'forceUpdate(...): Can only force an update on mounted or mounting ' +
88
+ 'components. This usually means you called forceUpdate() on an ' +
89
+ 'unmounted component.'
90
+ );
91
+ internalInstance.forceUpdate(callback && callback.bind(this));
92
+};
93
+
94
+/**
95
+ * Deprecated APIs. These APIs used to exist on classic React classes but since
96
+ * we would like to deprecate them, we're not going to move them over to this
97
+ * modern base class. Instead, we define a getter that warns if it's accessed.
98
+ */
99
+if (__DEV__) {
100
+ if (Object.defineProperty) {
101
+ var deprecatedAPIs = {
102
+ getDOMNode: 'getDOMNode',
103
+ isMounted: 'isMounted',
104
+ replaceState: 'replaceState',
105
+ setProps: 'setProps'
106
+ };
107
+ var defineDeprecationWarning = function(methodName, displayName) {
108
+ Object.defineProperty(ReactComponentBase.prototype, methodName, {
109
+ get: function() {
110
+ warning(
111
+ false,
112
+ '%s(...) is deprecated in plain JavaScript React classes.',
113
+ displayName
114
+ );
115
+ return undefined;
116
+ }
117
+ });
118
+ };
119
+ for (var methodName in deprecatedAPIs) {
120
+ if (deprecatedAPIs.hasOwnProperty(methodName)) {
121
+ defineDeprecationWarning(methodName, deprecatedAPIs[methodName]);
122
+ }
123
+ }
124
+ }
125
+}
126
+
127
+module.exports = ReactComponentBase;
src/modern/class/__tests__/ReactES6Class-test.js
new
+150
@@ -0,0 +1,150 @@
1
+/**
2
+ * Copyright 2013-2014, 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 mocks = require('mocks');
15
+
16
+var React;
17
+
18
+describe('ReactES6Class', function() {
19
+
20
+ var container;
21
+ var Inner;
22
+ var attachedListener = null;
23
+ var renderedName = null;
24
+
25
+ beforeEach(function() {
26
+ React = require('React');
27
+ container = document.createElement();
28
+ attachedListener = null;
29
+ renderedName = null;
30
+ Inner = class extends React.Component {
31
+ getName() {
32
+ return this.props.name;
33
+ }
34
+ render() {
35
+ attachedListener = this.props.onClick;
36
+ renderedName = this.props.name;
37
+ return <div className={this.props.name} />;
38
+ }
39
+ };
40
+ });
41
+
42
+ function test(element, expectedTag, expectedClassName) {
43
+ var instance = React.render(element, container);
44
+ expect(container.firstChild).not.toBeNull();
45
+ expect(container.firstChild.tagName).toBe(expectedTag);
46
+ expect(container.firstChild.className).toBe(expectedClassName);
47
+ return instance;
48
+ }
49
+
50
+ it('preserves the name of the class for use in error messages', function() {
51
+ class Foo extends React.Component { }
52
+ expect(Foo.name).toBe('Foo');
53
+ });
54
+
55
+ it('throws if no render function is defined', function() {
56
+ class Foo extends React.Component { }
57
+ expect(() => React.render(<Foo />, container)).toThrow();
58
+ });
59
+
60
+ it('renders a simple stateless component with prop', function() {
61
+ class Foo {
62
+ render() {
63
+ return <Inner name={this.props.bar} />;
64
+ }
65
+ }
66
+ test(<Foo bar="foo" />, 'DIV', 'foo');
67
+ test(<Foo bar="bar" />, 'DIV', 'bar');
68
+ });
69
+
70
+ it('renders using forceUpdate even when there is no state', function() {
71
+ class Foo extends React.Component {
72
+ constructor(props) {
73
+ this.mutativeValue = props.initialValue;
74
+ }
75
+ handleClick() {
76
+ this.mutativeValue = 'bar';
77
+ this.forceUpdate();
78
+ }
79
+ render() {
80
+ return (
81
+ <Inner
82
+ name={this.mutativeValue}
83
+ onClick={this.handleClick.bind(this)}
84
+ />
85
+ );
86
+ }
87
+ }
88
+ test(<Foo initialValue="foo" />, 'DIV', 'foo');
89
+ attachedListener();
90
+ expect(renderedName).toBe('bar');
91
+ });
92
+
93
+ it('should throw AND warn when trying to access classic APIs', function() {
94
+ spyOn(console, 'warn');
95
+ var instance = test(<Inner name="foo" />, 'DIV', 'foo');
96
+ expect(() => instance.getDOMNode()).toThrow();
97
+ expect(() => instance.replaceState({})).toThrow();
98
+ expect(() => instance.isMounted()).toThrow();
99
+ expect(() => instance.setProps({ name: 'bar' })).toThrow();
100
+ expect(console.warn.calls.length).toBe(4);
101
+ expect(console.warn.calls[0].args[0]).toContain(
102
+ 'getDOMNode(...) is deprecated in plain JavaScript React classes'
103
+ );
104
+ expect(console.warn.calls[1].args[0]).toContain(
105
+ 'replaceState(...) is deprecated in plain JavaScript React classes'
106
+ );
107
+ expect(console.warn.calls[2].args[0]).toContain(
108
+ 'isMounted(...) is deprecated in plain JavaScript React classes'
109
+ );
110
+ expect(console.warn.calls[3].args[0]).toContain(
111
+ 'setProps(...) is deprecated in plain JavaScript React classes'
112
+ );
113
+ });
114
+
115
+ it('supports this.context passed via getChildContext', function() {
116
+ class Bar {
117
+ render() {
118
+ return <div className={this.context.bar} />;
119
+ }
120
+ }
121
+ Bar.contextTypes = { bar: React.PropTypes.string };
122
+ class Foo {
123
+ getChildContext() {
124
+ return { bar: 'bar-through-context' };
125
+ }
126
+ render() {
127
+ return <Bar />;
128
+ }
129
+ }
130
+ Foo.childContextTypes = { bar: React.PropTypes.string };
131
+ test(<Foo />, 'DIV', 'bar-through-context');
132
+ });
133
+
134
+ it('supports classic refs', function() {
135
+ class Foo {
136
+ render() {
137
+ return <Inner name="foo" ref="inner" />;
138
+ }
139
+ }
140
+ var instance = test(<Foo />, 'DIV', 'foo');
141
+ expect(instance.refs.inner.getName()).toBe('foo');
142
+ });
143
+
144
+ it('supports drilling through to the DOM using findDOMNode', function() {
145
+ var instance = test(<Inner name="foo" />, 'DIV', 'foo');
146
+ var node = React.findDOMNode(instance);
147
+ expect(node).toBe(container.firstChild);
148
+ });
149
+
150
+});