Skip to content

Commit ff032dc

Browse files
committed
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.
1 parent 90053da commit ff032dc

File tree

5 files changed

+363
-68
lines changed

5 files changed

+363
-68
lines changed

src/browser/ui/React.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var DOMPropertyOperations = require('DOMPropertyOperations');
1717
var EventPluginUtils = require('EventPluginUtils');
1818
var ReactChildren = require('ReactChildren');
1919
var ReactComponent = require('ReactComponent');
20+
var ReactComponentBase = require('ReactComponentBase');
2021
var ReactClass = require('ReactClass');
2122
var ReactContext = require('ReactContext');
2223
var ReactCurrentOwner = require('ReactCurrentOwner');
@@ -57,6 +58,7 @@ var React = {
5758
count: ReactChildren.count,
5859
only: onlyChild
5960
},
61+
Component: ReactComponentBase,
6062
DOM: ReactDOM,
6163
PropTypes: ReactPropTypes,
6264
initializeTouchEvents: function(shouldUseTouch) {

src/classic/class/ReactClass.js

Lines changed: 6 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
"use strict";
1313

14+
var ReactComponentBase = require('ReactComponentBase');
1415
var ReactElement = require('ReactElement');
1516
var ReactErrorUtils = require('ReactErrorUtils');
1617
var ReactInstanceMap = require('ReactInstanceMap');
@@ -22,7 +23,6 @@ var invariant = require('invariant');
2223
var keyMirror = require('keyMirror');
2324
var keyOf = require('keyOf');
2425
var monitorCodeUse = require('monitorCodeUse');
25-
var warning = require('warning');
2626

2727
var MIXINS_KEY = keyOf({mixins: null});
2828

@@ -692,49 +692,11 @@ function bindAutoBindMethods(component) {
692692
}
693693

694694
/**
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.
696697
*/
697698
var ReactClassMixin = {
698699

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-
738700
/**
739701
* TODO: This will be deprecated because state should always keep a consistent
740702
* type signature and the only use case for this, is to avoid that.
@@ -743,38 +705,15 @@ var ReactClassMixin = {
743705
var internalInstance = ReactInstanceMap.get(this);
744706
invariant(
745707
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.'
747710
);
748711
internalInstance.replaceState(
749712
newState,
750713
callback && callback.bind(this)
751714
);
752715
},
753716

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-
778717
/**
779718
* Checks whether or not this composite component is mounted.
780719
* @return {boolean} True if mounted, false otherwise.
@@ -829,6 +768,7 @@ var ReactClassMixin = {
829768
var ReactClassBase = function() {};
830769
assign(
831770
ReactClassBase.prototype,
771+
ReactComponentBase.prototype,
832772
ReactClassMixin
833773
);
834774

src/core/__tests__/ReactCompositeComponent-test.js

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,8 @@ describe('ReactCompositeComponent', function() {
297297
instance.forceUpdate();
298298
}).toThrow(
299299
'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.'
301302
);
302303
});
303304

@@ -327,10 +328,39 @@ describe('ReactCompositeComponent', function() {
327328
instance.setState({ value: 2 });
328329
}).toThrow(
329330
'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.'
331333
);
332334
});
333335

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+
334364
it('should not allow `setProps` on unmounted components', function() {
335365
var container = document.createElement('div');
336366
document.documentElement.appendChild(container);
@@ -623,6 +653,31 @@ describe('ReactCompositeComponent', function() {
623653
);
624654
});
625655

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+
626681
it('should update refs if shouldComponentUpdate gives false', function() {
627682
var Static = React.createClass({
628683
shouldComponentUpdate: function() {
@@ -659,4 +714,25 @@ describe('ReactCompositeComponent', function() {
659714
expect(comp.refs.static1.getDOMNode().textContent).toBe('A');
660715
});
661716

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+
662738
});
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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;

0 commit comments

Comments
 (0)