Separate React Composite and Class
ReactClass is now composed by ReactCompositeComponent rather than inherit from it. The state transition functions currently use ReactInstanceMap to map an instance back to an internal representation. I updated some tests to use public APIs. Other unit tests still reach into internals but now we can find them using ReactInstanceMap. I will do more cleanup in follow ups. The purpose of this diff is to preserve semantics and most of the existing code. This effectively enables support for ES6 classes. All you would need to expose is ReactClassMixin.
Sebastian Markbage committed
Nov 6, 2014 at 19:49 UTC
27c482a2b83464eb89f5cdfed30e63a7b3ce134e
23 files changed
+828
-449
src/browser/ui/ReactBrowserComponentMixin.js
+1
-5
@@ -11,7 +11,6 @@
11
12
"use strict";
13
14
-var ReactEmptyComponent = require('ReactEmptyComponent');
14
var ReactMount = require('ReactMount');
15
16
var invariant = require('invariant');
@@ -29,10 +28,7 @@ var ReactBrowserComponentMixin = {
28
this.isMounted(),
29
'getDOMNode(): A component must be mounted to have a DOM node.'
30
);
32
- if (ReactEmptyComponent.isNullComponentID(this._rootNodeID)) {
33
- return null;
34
- }
35
- return ReactMount.getNode(this._rootNodeID);
31
+ return ReactMount.getNodeFromInstance(this);
32
}
33
};
34
src/browser/ui/ReactMount.js
+30
-2
@@ -15,7 +15,9 @@ var DOMProperty = require('DOMProperty');
15
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
16
var ReactCurrentOwner = require('ReactCurrentOwner');
17
var ReactElement = require('ReactElement');
18
+var ReactEmptyComponent = require('ReactEmptyComponent');
19
var ReactInstanceHandles = require('ReactInstanceHandles');
20
+var ReactInstanceMap = require('ReactInstanceMap');
21
var ReactPerf = require('ReactPerf');
22
23
var containsNode = require('containsNode');
@@ -125,6 +127,30 @@ function getNode(id) {
127
return nodeCache[id];
128
}
129
130
+/**
131
+ * Finds the node with the supplied public React instance.
132
+ *
133
+ * @param {*} instance A public React instance.
134
+ * @return {?DOMElement} DOM node with the suppled `id`.
135
+ * @internal
136
+ */
137
+function getNodeFromInstance(instance) {
138
+ // This instance can currently be either a public or private instance since
139
+ // native nodes are still public.
140
+ var id = instance._rootNodeID;
141
+ // TODO: Once these are only public instances, remove this conditional.
142
+ if (id == null) {
143
+ id = ReactInstanceMap.get(instance)._rootNodeID;
144
+ }
145
+ if (ReactEmptyComponent.isNullComponentID(id)) {
146
+ return null;
147
+ }
148
+ if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) {
149
+ nodeCache[id] = ReactMount.findReactNodeByID(id);
150
+ }
151
+ return nodeCache[id];
152
+}
153
+
154
/**
155
* A node is "valid" if it is contained by a currently mounted container.
156
*
@@ -358,7 +384,7 @@ var ReactMount = {
384
nextElement,
385
container,
386
callback
361
- );
387
+ ).getPublicInstance();
388
} else {
389
ReactMount.unmountComponentAtNode(container);
390
}
@@ -374,7 +400,7 @@ var ReactMount = {
400
nextElement,
401
container,
402
shouldReuseMarkup
377
- );
403
+ ).getPublicInstance();
404
callback && callback.call(component);
405
return component;
406
},
@@ -677,6 +703,8 @@ var ReactMount = {
703
704
getNode: getNode,
705
706
+ getNodeFromInstance: getNodeFromInstance,
707
+
708
purgeID: purgeID
709
};
710
src/browser/ui/__tests__/ReactDOMComponent-test.js
+66
-63
@@ -21,41 +21,39 @@ describe('ReactDOMComponent', function() {
21
describe('updateDOM', function() {
22
var React;
23
var ReactTestUtils;
24
- var transaction;
24
25
beforeEach(function() {
26
React = require('React');
27
ReactTestUtils = require('ReactTestUtils');
29
-
30
- var ReactReconcileTransaction = require('ReactReconcileTransaction');
31
- transaction = new ReactReconcileTransaction();
28
});
29
30
it("should handle className", function() {
35
- var stub = ReactTestUtils.renderIntoDocument(<div style={{}} />);
36
-
37
- stub.receiveComponent({props: { className: 'foo' }}, transaction);
38
- expect(stub.getDOMNode().className).toEqual('foo');
39
- stub.receiveComponent({props: { className: 'bar' }}, transaction);
40
- expect(stub.getDOMNode().className).toEqual('bar');
41
- stub.receiveComponent({props: { className: null }}, transaction);
42
- expect(stub.getDOMNode().className).toEqual('');
31
+ var container = document.createElement('div');
32
+ React.render(<div style={{}} />, container);
33
+
34
+ React.render(<div className={'foo'} />, container);
35
+ expect(container.firstChild.className).toEqual('foo');
36
+ React.render(<div className={'bar'} />, container);
37
+ expect(container.firstChild.className).toEqual('bar');
38
+ React.render(<div className={null} />, container);
39
+ expect(container.firstChild.className).toEqual('');
40
});
41
42
it("should gracefully handle various style value types", function() {
46
- var stub = ReactTestUtils.renderIntoDocument(<div style={{}} />);
47
- var stubStyle = stub.getDOMNode().style;
43
+ var container = document.createElement('div');
44
+ React.render(<div style={{}} />, container);
45
+ var stubStyle = container.firstChild.style;
46
47
// set initial style
48
var setup = { display: 'block', left: '1', top: 2, fontFamily: 'Arial' };
51
- stub.receiveComponent({props: { style: setup }}, transaction);
49
+ React.render(<div style={setup} />, container);
50
expect(stubStyle.display).toEqual('block');
51
expect(stubStyle.left).toEqual('1px');
52
expect(stubStyle.fontFamily).toEqual('Arial');
53
54
// reset the style to their default state
55
var reset = { display: '', left: null, top: false, fontFamily: true };
58
- stub.receiveComponent({props: { style: reset }}, transaction);
56
+ React.render(<div style={reset} />, container);
57
expect(stubStyle.display).toEqual('');
58
expect(stubStyle.left).toEqual('');
59
expect(stubStyle.top).toEqual('');
@@ -64,34 +62,35 @@ describe('ReactDOMComponent', function() {
62
63
it("should update styles when mutating style object", function() {
64
var styles = { display: 'none', fontFamily: 'Arial', lineHeight: 1.2 };
67
- var stub = ReactTestUtils.renderIntoDocument(<div style={styles} />);
65
+ var container = document.createElement('div');
66
+ React.render(<div style={styles} />, container);
67
69
- var stubStyle = stub.getDOMNode().style;
68
+ var stubStyle = container.firstChild.style;
69
stubStyle.display = styles.display;
70
stubStyle.fontFamily = styles.fontFamily;
71
72
styles.display = 'block';
73
75
- stub.receiveComponent({props: { style: styles }}, transaction);
74
+ React.render(<div style={styles} />, container);
75
expect(stubStyle.display).toEqual('block');
76
expect(stubStyle.fontFamily).toEqual('Arial');
77
expect(stubStyle.lineHeight).toEqual('1.2');
78
79
styles.fontFamily = 'Helvetica';
80
82
- stub.receiveComponent({props: { style: styles }}, transaction);
81
+ React.render(<div style={styles} />, container);
82
expect(stubStyle.display).toEqual('block');
83
expect(stubStyle.fontFamily).toEqual('Helvetica');
84
expect(stubStyle.lineHeight).toEqual('1.2');
85
86
styles.lineHeight = 0.5;
87
89
- stub.receiveComponent({props: { style: styles }}, transaction);
88
+ React.render(<div style={styles} />, container);
89
expect(stubStyle.display).toEqual('block');
90
expect(stubStyle.fontFamily).toEqual('Helvetica');
91
expect(stubStyle.lineHeight).toEqual('0.5');
92
94
- stub.receiveComponent({props: { style: undefined }}, transaction);
93
+ React.render(<div style={undefined} />, container);
94
expect(stubStyle.display).toBe('');
95
expect(stubStyle.fontFamily).toBe('');
96
expect(stubStyle.lineHeight).toBe('');
@@ -99,92 +98,96 @@ describe('ReactDOMComponent', function() {
98
99
it("should update styles if initially null", function() {
100
var styles = null;
102
- var stub = ReactTestUtils.renderIntoDocument(<div style={styles} />);
101
+ var container = document.createElement('div');
102
+ React.render(<div style={styles} />, container);
103
104
- var stubStyle = stub.getDOMNode().style;
104
+ var stubStyle = container.firstChild.style;
105
106
styles = {display: 'block'};
107
108
- stub.receiveComponent({props: { style: styles }}, transaction);
108
+ React.render(<div style={styles} />, container);
109
expect(stubStyle.display).toEqual('block');
110
});
111
112
it("should remove attributes", function() {
113
- var stub = ReactTestUtils.renderIntoDocument(<img height='17' />);
113
+ var container = document.createElement('div');
114
+ React.render(<img height='17' />, container);
115
115
- expect(stub.getDOMNode().hasAttribute('height')).toBe(true);
116
- stub.receiveComponent({props: {}}, transaction);
117
- expect(stub.getDOMNode().hasAttribute('height')).toBe(false);
116
+ expect(container.firstChild.hasAttribute('height')).toBe(true);
117
+ React.render(<img />, container);
118
+ expect(container.firstChild.hasAttribute('height')).toBe(false);
119
});
120
121
it("should remove properties", function() {
121
- var stub = ReactTestUtils.renderIntoDocument(<div className='monkey' />);
122
+ var container = document.createElement('div');
123
+ React.render(<div className='monkey' />, container);
124
123
- expect(stub.getDOMNode().className).toEqual('monkey');
124
- stub.receiveComponent({props: {}}, transaction);
125
- expect(stub.getDOMNode().className).toEqual('');
125
+ expect(container.firstChild.className).toEqual('monkey');
126
+ React.render(<div />, container);
127
+ expect(container.firstChild.className).toEqual('');
128
});
129
130
it("should clear a single style prop when changing 'style'", function() {
131
var styles = {display: 'none', color: 'red'};
130
- var stub = ReactTestUtils.renderIntoDocument(<div style={styles} />);
132
+ var container = document.createElement('div');
133
+ React.render(<div style={styles} />, container);
134
132
- var stubStyle = stub.getDOMNode().style;
135
+ var stubStyle = container.firstChild.style;
136
137
styles = {color: 'green'};
135
- stub.receiveComponent({props: { style: styles }}, transaction);
138
+ React.render(<div style={styles} />, container);
139
expect(stubStyle.display).toEqual('');
140
expect(stubStyle.color).toEqual('green');
141
});
142
143
it("should clear all the styles when removing 'style'", function() {
144
var styles = {display: 'none', color: 'red'};
142
- var stub = ReactTestUtils.renderIntoDocument(<div style={styles} />);
145
+ var container = document.createElement('div');
146
+ React.render(<div style={styles} />, container);
147
144
- var stubStyle = stub.getDOMNode().style;
148
+ var stubStyle = container.firstChild.style;
149
146
- stub.receiveComponent({props: {}}, transaction);
150
+ React.render(<div />, container);
151
expect(stubStyle.display).toEqual('');
152
expect(stubStyle.color).toEqual('');
153
});
154
155
it("should empty element when removing innerHTML", function() {
152
- var stub = ReactTestUtils.renderIntoDocument(
153
- <div dangerouslySetInnerHTML={{__html: ':)'}} />
154
- );
156
+ var container = document.createElement('div');
157
+ React.render(<div dangerouslySetInnerHTML={{__html: ':)'}} />, container);
158
156
- expect(stub.getDOMNode().innerHTML).toEqual(':)');
157
- stub.receiveComponent({props: {}}, transaction);
158
- expect(stub.getDOMNode().innerHTML).toEqual('');
159
+ expect(container.firstChild.innerHTML).toEqual(':)');
160
+ React.render(<div />, container);
161
+ expect(container.firstChild.innerHTML).toEqual('');
162
});
163
164
it("should transition from string content to innerHTML", function() {
162
- var stub = ReactTestUtils.renderIntoDocument(
163
- <div>hello</div>
164
- );
165
+ var container = document.createElement('div');
166
+ React.render(<div>hello</div>, container);
167
166
- expect(stub.getDOMNode().innerHTML).toEqual('hello');
167
- stub.receiveComponent(
168
- {props: {dangerouslySetInnerHTML: {__html: 'goodbye'}}},
169
- transaction
168
+ expect(container.firstChild.innerHTML).toEqual('hello');
169
+ React.render(
170
+ <div dangerouslySetInnerHTML={{__html: 'goodbye'}} />,
171
+ container
172
);
171
- expect(stub.getDOMNode().innerHTML).toEqual('goodbye');
173
+ expect(container.firstChild.innerHTML).toEqual('goodbye');
174
});
175
176
it("should transition from innerHTML to string content", function() {
175
- var stub = ReactTestUtils.renderIntoDocument(
176
- <div dangerouslySetInnerHTML={{__html: 'bonjour'}} />
177
- );
177
+ var container = document.createElement('div');
178
+ React.render(<div dangerouslySetInnerHTML={{__html: 'bonjour'}} />
179
+ , container);
180
179
- expect(stub.getDOMNode().innerHTML).toEqual('bonjour');
180
- stub.receiveComponent({props: {children: 'adieu'}}, transaction);
181
- expect(stub.getDOMNode().innerHTML).toEqual('adieu');
181
+ expect(container.firstChild.innerHTML).toEqual('bonjour');
182
+ React.render(<div>adieu</div>, container);
183
+ expect(container.firstChild.innerHTML).toEqual('adieu');
184
});
185
186
it("should not incur unnecessary DOM mutations", function() {
185
- var stub = ReactTestUtils.renderIntoDocument(<div value="" />);
187
+ var container = document.createElement('div');
188
+ React.render(<div value="" />, container);
189
187
- var node = stub.getDOMNode();
190
+ var node = container.firstChild;
191
var nodeValue = ''; // node.value always returns undefined
192
var nodeValueSetter = mocks.getMockFunction();
193
Object.defineProperty(node, 'value', {
@@ -196,10 +199,10 @@ describe('ReactDOMComponent', function() {
199
})
200
});
201
199
- stub.receiveComponent({props: {value: ''}}, transaction);
202
+ React.render(<div value="" />, container);
203
expect(nodeValueSetter.mock.calls.length).toBe(0);
204
202
- stub.receiveComponent({props: {}}, transaction);
205
+ React.render(<div />, container);
206
expect(nodeValueSetter.mock.calls.length).toBe(1);
207
});
208
});
src/browser/ui/__tests__/ReactRenderDocument-test.js
+5
-1
@@ -14,6 +14,7 @@
14
"use strict";
15
16
var React;
17
+var ReactInstanceMap;
18
var ReactMount;
19
20
var getTestDocument;
@@ -32,6 +33,7 @@ describe('rendering React components at document', function() {
33
require('mock-modules').dumpCache();
34
35
React = require('React');
36
+ ReactInstanceMap = require('ReactInstanceMap');
37
ReactMount = require('ReactMount');
38
getTestDocument = require('getTestDocument');
39
@@ -61,8 +63,10 @@ describe('rendering React components at document', function() {
63
var component = React.render(<Root />, testDocument);
64
expect(testDocument.body.innerHTML).toBe('Hello world');
65
66
+ // TODO: This is a bad test. I have no idea what this is testing.
67
+ // Node IDs is an implementation detail and not part of the public API.
68
var componentID = ReactMount.getReactRootID(testDocument);
65
- expect(componentID).toBe(component._rootNodeID);
69
+ expect(componentID).toBe(ReactInstanceMap.get(component)._rootNodeID);
70
});
71
72
it('should not be able to unmount component from document node', function() {
src/class/ReactClass.js
+224
-5
@@ -11,8 +11,10 @@
11
12
"use strict";
13
14
-var ReactCompositeComponent = require('ReactCompositeComponent');
14
var ReactElement = require('ReactElement');
15
+var ReactErrorUtils = require('ReactErrorUtils');
16
+var ReactInstanceMap = require('ReactInstanceMap');
17
+var ReactPropTransferer = require('ReactPropTransferer');
18
var ReactPropTypeLocations = require('ReactPropTypeLocations');
19
var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
20
@@ -22,6 +24,7 @@ var keyMirror = require('keyMirror');
24
var keyOf = require('keyOf');
25
var monitorCodeUse = require('monitorCodeUse');
26
var mapObject = require('mapObject');
27
+var warning = require('warning');
28
29
var MIXINS_KEY = keyOf({mixins: null});
30
@@ -398,7 +401,7 @@ function validateMethodOverride(proto, name) {
401
null;
402
403
// Disallow overriding of base class methods unless explicitly allowed.
401
- if (ReactCompositeComponent.Base.prototype.hasOwnProperty(name)) {
404
+ if (ReactClassMixin.hasOwnProperty(name)) {
405
invariant(
406
specPolicy === SpecPolicy.OVERRIDE_BASE,
407
'ReactClassInterface: You are attempting to override ' +
@@ -621,6 +624,218 @@ function createChainedFunction(one, two) {
624
};
625
}
626
627
+/**
628
+ * Binds a method to the component.
629
+ *
630
+ * @param {object} component Component whose method is going to be bound.
631
+ * @param {function} method Method to be bound.
632
+ * @return {function} The bound method.
633
+ */
634
+function bindAutoBindMethod(component, method) {
635
+ var boundMethod = method.bind(component);
636
+ if (__DEV__) {
637
+ boundMethod.__reactBoundContext = component;
638
+ boundMethod.__reactBoundMethod = method;
639
+ boundMethod.__reactBoundArguments = null;
640
+ var componentName = component.constructor.displayName;
641
+ var _bind = boundMethod.bind;
642
+ boundMethod.bind = function(newThis, ...args) {
643
+ // User is trying to bind() an autobound method; we effectively will
644
+ // ignore the value of "this" that the user is trying to use, so
645
+ // let's warn.
646
+ if (newThis !== component && newThis !== null) {
647
+ monitorCodeUse('react_bind_warning', { component: componentName });
648
+ console.warn(
649
+ 'bind(): React component methods may only be bound to the ' +
650
+ 'component instance. See ' + componentName
651
+ );
652
+ } else if (!args.length) {
653
+ monitorCodeUse('react_bind_warning', { component: componentName });
654
+ console.warn(
655
+ 'bind(): You are binding a component method to the component. ' +
656
+ 'React does this for you automatically in a high-performance ' +
657
+ 'way, so you can safely remove this call. See ' + componentName
658
+ );
659
+ return boundMethod;
660
+ }
661
+ var reboundMethod = _bind.apply(boundMethod, arguments);
662
+ reboundMethod.__reactBoundContext = component;
663
+ reboundMethod.__reactBoundMethod = method;
664
+ reboundMethod.__reactBoundArguments = args;
665
+ return reboundMethod;
666
+ };
667
+ }
668
+ return boundMethod;
669
+}
670
+
671
+/**
672
+ * Binds all auto-bound methods in a component.
673
+ *
674
+ * @param {object} component Component whose method is going to be bound.
675
+ */
676
+function bindAutoBindMethods(component) {
677
+ for (var autoBindKey in component.__reactAutoBindMap) {
678
+ if (component.__reactAutoBindMap.hasOwnProperty(autoBindKey)) {
679
+ var method = component.__reactAutoBindMap[autoBindKey];
680
+ component[autoBindKey] = bindAutoBindMethod(
681
+ component,
682
+ ReactErrorUtils.guard(
683
+ method,
684
+ component.constructor.displayName + '.' + autoBindKey
685
+ )
686
+ );
687
+ }
688
+ }
689
+}
690
+
691
+/**
692
+ * @lends {ReactClass.prototype}
693
+ */
694
+var ReactClassMixin = {
695
+
696
+ /**
697
+ * Sets a subset of the state. Always use this or `replaceState` to mutate
698
+ * state. You should treat `this.state` as immutable.
699
+ *
700
+ * There is no guarantee that `this.state` will be immediately updated, so
701
+ * accessing `this.state` after calling this method may return the old value.
702
+ *
703
+ * There is no guarantee that calls to `setState` will run synchronously,
704
+ * as they may eventually be batched together. You can provide an optional
705
+ * callback that will be executed when the call to setState is actually
706
+ * completed.
707
+ *
708
+ * @param {object} partialState Next partial state to be merged with state.
709
+ * @param {?function} callback Called after state is updated.
710
+ * @final
711
+ * @protected
712
+ */
713
+ setState: function(partialState, callback) {
714
+ invariant(
715
+ typeof partialState === 'object' || partialState == null,
716
+ 'setState(...): takes an object of state variables to update.'
717
+ );
718
+ if (__DEV__) {
719
+ warning(
720
+ partialState != null,
721
+ 'setState(...): You passed an undefined or null state object; ' +
722
+ 'instead, use forceUpdate().'
723
+ );
724
+ }
725
+ var internalInstance = ReactInstanceMap.get(this);
726
+ invariant(
727
+ internalInstance,
728
+ 'setState(...): Can only update a mounted or mounting component.'
729
+ );
730
+ internalInstance.setState(
731
+ partialState, callback && callback.bind(this)
732
+ );
733
+ },
734
+
735
+ /**
736
+ * TODO: This will be deprecated because state should always keep a consistent
737
+ * type signature and the only use case for this, is to avoid that.
738
+ */
739
+ replaceState: function(newState, callback) {
740
+ var internalInstance = ReactInstanceMap.get(this);
741
+ invariant(
742
+ internalInstance,
743
+ 'replaceState(...): Can only update a mounted or mounting component.'
744
+ );
745
+ internalInstance.replaceState(
746
+ newState,
747
+ callback && callback.bind(this)
748
+ );
749
+ },
750
+
751
+ /**
752
+ * Forces an update. This should only be invoked when it is known with
753
+ * certainty that we are **not** in a DOM transaction.
754
+ *
755
+ * You may want to call this when you know that some deeper aspect of the
756
+ * component's state has changed but `setState` was not called.
757
+ *
758
+ * This will not invoke `shouldUpdateComponent`, but it will invoke
759
+ * `componentWillUpdate` and `componentDidUpdate`.
760
+ *
761
+ * @param {?function} callback Called after update is complete.
762
+ * @final
763
+ * @protected
764
+ */
765
+ forceUpdate: function(callback) {
766
+ var internalInstance = ReactInstanceMap.get(this);
767
+ invariant(
768
+ internalInstance,
769
+ 'forceUpdate(...): Can only force an update on mounted or mounting ' +
770
+ 'components.'
771
+ );
772
+ internalInstance.forceUpdate(callback && callback.bind(this));
773
+ },
774
+
775
+ /**
776
+ * Checks whether or not this composite component is mounted.
777
+ * @return {boolean} True if mounted, false otherwise.
778
+ * @protected
779
+ * @final
780
+ */
781
+ isMounted: function() {
782
+ var internalInstance = ReactInstanceMap.get(this);
783
+ // In theory, isMounted is always true if it exists in the map.
784
+ // TODO: Remove the internal isMounted method.
785
+ return internalInstance && internalInstance.isMounted();
786
+ },
787
+
788
+ /**
789
+ * Sets a subset of the props.
790
+ *
791
+ * @param {object} partialProps Subset of the next props.
792
+ * @param {?function} callback Called after props are updated.
793
+ * @final
794
+ * @public
795
+ * @deprecated
796
+ */
797
+ setProps: function(partialProps, callback) {
798
+ var internalInstance = ReactInstanceMap.get(this);
799
+ invariant(
800
+ internalInstance,
801
+ 'setProps(...): Can only update a mounted component.'
802
+ );
803
+ internalInstance.setProps(
804
+ partialProps,
805
+ callback && callback.bind(this)
806
+ );
807
+ },
808
+
809
+ /**
810
+ * Replace all the props.
811
+ *
812
+ * @param {object} newProps Subset of the next props.
813
+ * @param {?function} callback Called after props are updated.
814
+ * @final
815
+ * @public
816
+ * @deprecated
817
+ */
818
+ replaceProps: function(newProps, callback) {
819
+ ReactInstanceMap.get(this).replaceProps(
820
+ newProps,
821
+ callback && callback.bind(this)
822
+ );
823
+ }
824
+};
825
+
826
+var ReactClassBase = function() {};
827
+assign(
828
+ ReactClassBase.prototype,
829
+ ReactPropTransferer.Mixin,
830
+ ReactClassMixin
831
+);
832
+
833
+/**
834
+ * Module for creating composite components.
835
+ *
836
+ * @class ReactClass
837
+ * @extends ReactPropTransferer
838
+ */
839
var ReactClass = {
840
841
/**
@@ -633,10 +848,14 @@ var ReactClass = {
848
createClass: function(spec) {
849
var Constructor = function(props) {
850
// This constructor is overridden by mocks. The argument is used
636
- // by mocks to assert on what gets mounted. This will later be used
637
- // by the stand-alone class implementation.
851
+ // by mocks to assert on what gets mounted.
852
+
853
+ // Wire up auto-binding
854
+ if (this.__reactAutoBindMap) {
855
+ bindAutoBindMethods(this);
856
+ }
857
};
639
- Constructor.prototype = new ReactCompositeComponent.Base();
858
+ Constructor.prototype = new ReactClassBase();
859
Constructor.prototype.constructor = Constructor;
860
861
injectedMixins.forEach(
src/core/ReactComponent.js
+12
@@ -442,6 +442,18 @@ var ReactComponent = {
442
return null;
443
}
444
return owner.refs[ref];
445
+ },
446
+
447
+ /**
448
+ * Get the publicly accessible representation of this component - i.e. what
449
+ * is exposed by refs and renderComponent. Can be null for stateless
450
+ * components.
451
+ *
452
+ * @return {?ReactComponent} the actual sibling Component.
453
+ * @internal
454
+ */
455
+ getPublicInstance: function() {
456
+ return this;
457
}
458
}
459
};
src/core/ReactCompositeComponent.js
+133
-211
@@ -16,37 +16,31 @@ var ReactContext = require('ReactContext');
16
var ReactCurrentOwner = require('ReactCurrentOwner');
17
var ReactElement = require('ReactElement');
18
var ReactEmptyComponent = require('ReactEmptyComponent');
19
-var ReactErrorUtils = require('ReactErrorUtils');
19
+var ReactInstanceMap = require('ReactInstanceMap');
20
var ReactOwner = require('ReactOwner');
21
var ReactPerf = require('ReactPerf');
22
-var ReactPropTransferer = require('ReactPropTransferer');
22
var ReactPropTypeLocations = require('ReactPropTypeLocations');
23
var ReactUpdates = require('ReactUpdates');
24
25
var assign = require('Object.assign');
27
-var instantiateReactComponent = require('instantiateReactComponent');
26
var invariant = require('invariant');
27
var keyMirror = require('keyMirror');
30
-var monitorCodeUse = require('monitorCodeUse');
28
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
29
var warning = require('warning');
30
31
function getDeclarationErrorAddendum(component) {
32
var owner = component._owner || null;
36
- if (owner && owner.constructor && owner.constructor.displayName) {
37
- return ' Check the render method of `' + owner.constructor.displayName +
38
- '`.';
33
+ if (owner) {
34
+ var constructor = owner._instance.constructor;
35
+ if (constructor && constructor.displayName) {
36
+ return ' Check the render method of `' + constructor.displayName + '`.';
37
+ }
38
}
39
return '';
40
}
41
42
function validateLifeCycleOnReplaceState(instance) {
43
var compositeLifeCycleState = instance._compositeLifeCycleState;
45
- invariant(
46
- instance.isMounted() ||
47
- compositeLifeCycleState === CompositeLifeCycle.MOUNTING,
48
- 'replaceState(...): Can only update a mounted or mounting component.'
49
- );
44
invariant(
45
ReactCurrentOwner.current == null,
46
'replaceState(...): Cannot update during an existing state transition ' +
@@ -106,7 +100,9 @@ var CompositeLifeCycle = keyMirror({
100
/**
101
* @lends {ReactCompositeComponent.prototype}
102
*/
109
-var ReactCompositeComponentMixin = {
103
+var ReactCompositeComponentMixin = assign({},
104
+ ReactComponent.Mixin,
105
+ ReactOwner.Mixin, {
106
107
/**
108
* Base constructor for all composite component.
@@ -116,18 +112,16 @@ var ReactCompositeComponentMixin = {
112
* @internal
113
*/
114
construct: function(element) {
119
- // Children can be either an array or more than one argument
120
- ReactComponent.Mixin.construct.apply(this, arguments);
121
- ReactOwner.Mixin.construct.apply(this, arguments);
115
+ this._instance.props = element.props;
116
+ this._instance.state = null;
117
+ this._instance.context = null;
118
123
- this.state = null;
119
this._pendingState = null;
125
-
126
- // This is the public post-processed context. The real context and pending
127
- // context lives on the element.
128
- this.context = null;
129
-
120
this._compositeLifeCycleState = null;
121
+
122
+ // Children can be either an array or more than one argument
123
+ ReactComponent.Mixin.construct.apply(this, arguments);
124
+ ReactOwner.Mixin.construct.apply(this, arguments);
125
},
126
127
/**
@@ -161,47 +155,48 @@ var ReactCompositeComponentMixin = {
155
transaction,
156
mountDepth
157
);
164
- this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING;
158
166
- if (this.__reactAutoBindMap) {
167
- this._bindAutoBindMethods();
168
- }
159
+ var inst = this._instance;
160
+
161
+ // Store a reference from the instance back to the internal representation
162
+ ReactInstanceMap.set(inst, this);
163
170
- this.context = this._processContext(this._currentElement._context);
171
- this.props = this._processProps(this.props);
164
+ this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING;
165
173
- this.state = this.getInitialState ? this.getInitialState() : null;
166
+ inst.context = this._processContext(this._currentElement._context);
167
+ inst.props = this._processProps(this._currentElement.props);
168
169
+ var initialState = inst.getInitialState ? inst.getInitialState() : null;
170
if (__DEV__) {
171
// We allow auto-mocks to proceed as if they're returning null.
177
- if (typeof this.state === 'undefined' &&
178
- this.getInitialState && this.getInitialState._isMockFunction) {
172
+ if (typeof initialState === 'undefined' &&
173
+ inst.getInitialState._isMockFunction) {
174
// This is probably bad practice. Consider warning here and
175
// deprecating this convenience.
181
- this.state = null;
176
+ initialState = null;
177
}
178
}
184
-
179
invariant(
186
- typeof this.state === 'object' && !Array.isArray(this.state),
180
+ typeof initialState === 'object' && !Array.isArray(initialState),
181
'%s.getInitialState(): must return an object or null',
188
- this.constructor.displayName || 'ReactCompositeComponent'
182
+ inst.constructor.displayName || 'ReactCompositeComponent'
183
);
184
+ inst.state = initialState;
185
186
this._pendingState = null;
187
this._pendingForceUpdate = false;
188
194
- if (this.componentWillMount) {
195
- this.componentWillMount();
189
+ if (inst.componentWillMount) {
190
+ inst.componentWillMount();
191
// When mounting, calls to `setState` by `componentWillMount` will set
192
// `this._pendingState` without triggering a re-render.
193
if (this._pendingState) {
199
- this.state = this._pendingState;
194
+ inst.state = this._pendingState;
195
this._pendingState = null;
196
}
197
}
198
204
- this._renderedComponent = instantiateReactComponent(
199
+ this._renderedComponent = this._instantiateReactComponent(
200
this._renderValidatedComponent(),
201
this._currentElement.type // The wrapping type
202
);
@@ -213,8 +208,8 @@ var ReactCompositeComponentMixin = {
208
transaction,
209
mountDepth + 1
210
);
216
- if (this.componentDidMount) {
217
- transaction.getReactMountReady().enqueue(this.componentDidMount, this);
211
+ if (inst.componentDidMount) {
212
+ transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
213
}
214
return markup;
215
}
@@ -227,9 +222,11 @@ var ReactCompositeComponentMixin = {
222
* @internal
223
*/
224
unmountComponent: function() {
225
+ var inst = this._instance;
226
+
227
this._compositeLifeCycleState = CompositeLifeCycle.UNMOUNTING;
231
- if (this.componentWillUnmount) {
232
- this.componentWillUnmount();
228
+ if (inst.componentWillUnmount) {
229
+ inst.componentWillUnmount();
230
}
231
this._compositeLifeCycleState = null;
232
@@ -238,23 +235,23 @@ var ReactCompositeComponentMixin = {
235
236
ReactComponent.Mixin.unmountComponent.call(this);
237
241
- // Some existing components rely on this.props even after they've been
238
+ // Delete the reference from the instance to this internal representation
239
+ // which allow the internals to be properly cleaned up even if the user
240
+ // leaks a reference to the public instance.
241
+ ReactInstanceMap.remove(inst);
242
+
243
+ // Some existing components rely on inst.props even after they've been
244
// destroyed (in event handlers).
243
- // TODO: this.props = null;
244
- // TODO: this.state = null;
245
+ // TODO: inst.props = null;
246
+ // TODO: inst.state = null;
247
+ // TODO: inst.context = null;
248
},
249
250
/**
248
- * Sets a subset of the state. Always use this or `replaceState` to mutate
249
- * state. You should treat `this.state` as immutable.
250
- *
251
- * There is no guarantee that `this.state` will be immediately updated, so
252
- * accessing `this.state` after calling this method may return the old value.
253
- *
254
- * There is no guarantee that calls to `setState` will run synchronously,
255
- * as they may eventually be batched together. You can provide an optional
256
- * callback that will be executed when the call to setState is actually
257
- * completed.
251
+ * Sets a subset of the state. This only exists because _pendingState is
252
+ * internal. This provides a merging strategy that is not available to deep
253
+ * properties which is confusing. TODO: Expose pendingState or don't use it
254
+ * during the merge.
255
*
256
* @param {object} partialState Next partial state to be merged with state.
257
* @param {?function} callback Called after state is updated.
@@ -262,20 +259,9 @@ var ReactCompositeComponentMixin = {
259
* @protected
260
*/
261
setState: function(partialState, callback) {
265
- invariant(
266
- typeof partialState === 'object' || partialState == null,
267
- 'setState(...): takes an object of state variables to update.'
268
- );
269
- if (__DEV__){
270
- warning(
271
- partialState != null,
272
- 'setState(...): You passed an undefined or null state object; ' +
273
- 'instead, use forceUpdate().'
274
- );
275
- }
262
// Merge with `_pendingState` if it exists, otherwise with existing state.
263
this.replaceState(
278
- assign({}, this._pendingState || this.state, partialState),
264
+ assign({}, this._pendingState || this._instance.state, partialState),
265
callback
266
);
267
},
@@ -306,6 +292,32 @@ var ReactCompositeComponentMixin = {
292
}
293
},
294
295
+ /**
296
+ * Forces an update. This should only be invoked when it is known with
297
+ * certainty that we are **not** in a DOM transaction.
298
+ *
299
+ * You may want to call this when you know that some deeper aspect of the
300
+ * component's state has changed but `setState` was not called.
301
+ *
302
+ * This will not invoke `shouldUpdateComponent`, but it will invoke
303
+ * `componentWillUpdate` and `componentDidUpdate`.
304
+ *
305
+ * @param {?function} callback Called after update is complete.isM
306
+ * @final
307
+ * @protected
308
+ */
309
+ forceUpdate: function(callback) {
310
+ var compositeLifeCycleState = this._compositeLifeCycleState;
311
+ invariant(
312
+ compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE &&
313
+ compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
314
+ 'forceUpdate(...): Cannot force an update while unmounting component ' +
315
+ 'or during an existing state transition (such as within `render`).'
316
+ );
317
+ this._pendingForceUpdate = true;
318
+ ReactUpdates.enqueueUpdate(this, callback);
319
+ },
320
+
321
/**
322
* Filters the context object to only contain keys specified in
323
* `contextTypes`, and asserts that they are valid.
@@ -316,7 +328,7 @@ var ReactCompositeComponentMixin = {
328
*/
329
_processContext: function(context) {
330
var maskedContext = null;
319
- var contextTypes = this.constructor.contextTypes;
331
+ var contextTypes = this._instance.constructor.contextTypes;
332
if (contextTypes) {
333
maskedContext = {};
334
for (var contextName in contextTypes) {
@@ -339,25 +351,26 @@ var ReactCompositeComponentMixin = {
351
* @private
352
*/
353
_processChildContext: function(currentContext) {
342
- var childContext = this.getChildContext && this.getChildContext();
343
- var displayName = this.constructor.displayName || 'ReactCompositeComponent';
354
+ var inst = this._instance;
355
+ var childContext = inst.getChildContext && inst.getChildContext();
356
+ var displayName = inst.constructor.displayName || 'ReactCompositeComponent';
357
if (childContext) {
358
invariant(
346
- typeof this.constructor.childContextTypes === 'object',
359
+ typeof inst.constructor.childContextTypes === 'object',
360
'%s.getChildContext(): childContextTypes must be defined in order to ' +
361
'use getChildContext().',
362
displayName
363
);
364
if (__DEV__) {
365
this._checkPropTypes(
353
- this.constructor.childContextTypes,
366
+ inst.constructor.childContextTypes,
367
childContext,
368
ReactPropTypeLocations.childContext
369
);
370
}
371
for (var name in childContext) {
372
invariant(
360
- name in this.constructor.childContextTypes,
373
+ name in inst.constructor.childContextTypes,
374
'%s.getChildContext(): key "%s" is not defined in childContextTypes.',
375
displayName,
376
name
@@ -379,7 +392,8 @@ var ReactCompositeComponentMixin = {
392
*/
393
_processProps: function(newProps) {
394
if (__DEV__) {
382
- var propTypes = this.constructor.propTypes;
395
+ var inst = this._instance;
396
+ var propTypes = inst.constructor.propTypes;
397
if (propTypes) {
398
this._checkPropTypes(propTypes, newProps, ReactPropTypeLocations.prop);
399
}
@@ -398,7 +412,7 @@ var ReactCompositeComponentMixin = {
412
_checkPropTypes: function(propTypes, props, location) {
413
// TODO: Stop validating prop types here and only use the element
414
// validation.
401
- var componentName = this.constructor.displayName;
415
+ var componentName = this._instance.constructor.displayName;
416
for (var propName in propTypes) {
417
if (propTypes.hasOwnProperty(propName)) {
418
var error =
@@ -497,8 +511,10 @@ var ReactCompositeComponentMixin = {
511
nextParentElement
512
);
513
500
- var prevContext = this.context;
501
- var prevProps = this.props;
514
+ var inst = this._instance;
515
+
516
+ var prevContext = inst.context;
517
+ var prevProps = inst.props;
518
var nextContext = prevContext;
519
var nextProps = prevProps;
520
// Distinguish between a props update versus a simple state update
@@ -507,25 +523,25 @@ var ReactCompositeComponentMixin = {
523
nextProps = this._processProps(nextParentElement.props);
524
525
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
510
- if (this.componentWillReceiveProps) {
511
- this.componentWillReceiveProps(nextProps, nextContext);
526
+ if (inst.componentWillReceiveProps) {
527
+ inst.componentWillReceiveProps(nextProps, nextContext);
528
}
529
}
530
531
this._compositeLifeCycleState = null;
532
517
- var nextState = this._pendingState || this.state;
533
+ var nextState = this._pendingState || inst.state;
534
this._pendingState = null;
535
536
var shouldUpdate =
537
this._pendingForceUpdate ||
522
- !this.shouldComponentUpdate ||
523
- this.shouldComponentUpdate(nextProps, nextState, nextContext);
538
+ !inst.shouldComponentUpdate ||
539
+ inst.shouldComponentUpdate(nextProps, nextState, nextContext);
540
541
if (__DEV__) {
542
if (typeof shouldUpdate === "undefined") {
543
console.warn(
528
- (this.constructor.displayName || 'ReactCompositeComponent') +
544
+ (inst.constructor.displayName || 'ReactCompositeComponent') +
545
'.shouldComponentUpdate(): Returned undefined instead of a ' +
546
'boolean value. Make sure to return true or false.'
547
);
@@ -536,9 +552,9 @@ var ReactCompositeComponentMixin = {
552
// If it's determined that a component should not update, we still want
553
// to set props and state but we shortcut the rest of the update.
554
this._currentElement = nextParentElement;
539
- this.props = nextProps;
540
- this.state = nextState;
541
- this.context = nextContext;
555
+ inst.props = nextProps;
556
+ inst.state = nextState;
557
+ inst.context = nextContext;
558
559
// Owner cannot change because shouldUpdateReactComponent doesn't allow
560
// it. TODO: Remove this._owner completely.
@@ -576,18 +592,20 @@ var ReactCompositeComponentMixin = {
592
nextContext,
593
transaction
594
) {
579
- var prevProps = this.props;
580
- var prevState = this.state;
581
- var prevContext = this.context;
595
+ var inst = this._instance;
596
+
597
+ var prevProps = inst.props;
598
+ var prevState = inst.state;
599
+ var prevContext = inst.context;
600
583
- if (this.componentWillUpdate) {
584
- this.componentWillUpdate(nextProps, nextState, nextContext);
601
+ if (inst.componentWillUpdate) {
602
+ inst.componentWillUpdate(nextProps, nextState, nextContext);
603
}
604
605
this._currentElement = nextElement;
588
- this.props = nextProps;
589
- this.state = nextState;
590
- this.context = nextContext;
606
+ inst.props = nextProps;
607
+ inst.state = nextState;
608
+ inst.context = nextContext;
609
610
// Owner cannot change because shouldUpdateReactComponent doesn't allow
611
// it. TODO: Remove this._owner completely.
@@ -595,10 +613,10 @@ var ReactCompositeComponentMixin = {
613
614
this._updateRenderedComponent(transaction);
615
598
- if (this.componentDidUpdate) {
616
+ if (inst.componentDidUpdate) {
617
transaction.getReactMountReady().enqueue(
600
- this.componentDidUpdate.bind(this, prevProps, prevState, prevContext),
601
- this
618
+ inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext),
619
+ inst
620
);
621
}
622
},
@@ -623,7 +641,7 @@ var ReactCompositeComponentMixin = {
641
var thisID = this._rootNodeID;
642
var prevComponentID = prevComponentInstance._rootNodeID;
643
prevComponentInstance.unmountComponent();
626
- this._renderedComponent = instantiateReactComponent(
644
+ this._renderedComponent = this._instantiateReactComponent(
645
nextRenderedElement,
646
this._currentElement.type
647
);
@@ -639,38 +657,6 @@ var ReactCompositeComponentMixin = {
657
}
658
},
659
642
- /**
643
- * Forces an update. This should only be invoked when it is known with
644
- * certainty that we are **not** in a DOM transaction.
645
- *
646
- * You may want to call this when you know that some deeper aspect of the
647
- * component's state has changed but `setState` was not called.
648
- *
649
- * This will not invoke `shouldUpdateComponent`, but it will invoke
650
- * `componentWillUpdate` and `componentDidUpdate`.
651
- *
652
- * @param {?function} callback Called after update is complete.
653
- * @final
654
- * @protected
655
- */
656
- forceUpdate: function(callback) {
657
- var compositeLifeCycleState = this._compositeLifeCycleState;
658
- invariant(
659
- this.isMounted() ||
660
- compositeLifeCycleState === CompositeLifeCycle.MOUNTING,
661
- 'forceUpdate(...): Can only force an update on mounted or mounting ' +
662
- 'components.'
663
- );
664
- invariant(
665
- compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING &&
666
- ReactCurrentOwner.current == null,
667
- 'forceUpdate(...): Cannot force an update while unmounting component ' +
668
- 'or within a `render` function.'
669
- );
670
- this._pendingForceUpdate = true;
671
- ReactUpdates.enqueueUpdate(this, callback);
672
- },
673
-
660
/**
661
* @private
662
*/
@@ -684,12 +670,13 @@ var ReactCompositeComponentMixin = {
670
this._currentElement._context
671
);
672
ReactCurrentOwner.current = this;
673
+ var inst = this._instance;
674
try {
688
- renderedComponent = this.render();
675
+ renderedComponent = inst.render();
676
if (__DEV__) {
677
// We allow auto-mocks to proceed as if they're returning null.
678
if (typeof renderedComponent === 'undefined' &&
692
- this.render._isMockFunction) {
679
+ inst.render._isMockFunction) {
680
// This is probably bad practice. Consider warning here and
681
// deprecating this convenience.
682
renderedComponent = null;
@@ -709,100 +696,35 @@ var ReactCompositeComponentMixin = {
696
ReactElement.isValidElement(renderedComponent),
697
'%s.render(): A valid ReactComponent must be returned. You may have ' +
698
'returned undefined, an array or some other invalid object.',
712
- this.constructor.displayName || 'ReactCompositeComponent'
699
+ inst.constructor.displayName || 'ReactCompositeComponent'
700
);
701
return renderedComponent;
702
}
703
),
704
705
/**
719
- * @private
706
+ * Get the publicly accessible representation of this component - i.e. what
707
+ * is exposed by refs and renderComponent. Can be null for stateless
708
+ * components.
709
+ *
710
+ * @return {ReactComponent} the public component instance.
711
+ * @internal
712
*/
721
- _bindAutoBindMethods: function() {
722
- for (var autoBindKey in this.__reactAutoBindMap) {
723
- if (!this.__reactAutoBindMap.hasOwnProperty(autoBindKey)) {
724
- continue;
725
- }
726
- var method = this.__reactAutoBindMap[autoBindKey];
727
- this[autoBindKey] = this._bindAutoBindMethod(ReactErrorUtils.guard(
728
- method,
729
- this.constructor.displayName + '.' + autoBindKey
730
- ));
731
- }
713
+ getPublicInstance: function() {
714
+ return this._instance;
715
},
716
734
- /**
735
- * Binds a method to the component.
736
- *
737
- * @param {function} method Method to be bound.
738
- * @private
739
- */
740
- _bindAutoBindMethod: function(method) {
741
- var component = this;
742
- var boundMethod = method.bind(component);
743
- if (__DEV__) {
744
- boundMethod.__reactBoundContext = component;
745
- boundMethod.__reactBoundMethod = method;
746
- boundMethod.__reactBoundArguments = null;
747
- var componentName = component.constructor.displayName;
748
- var _bind = boundMethod.bind;
749
- boundMethod.bind = function(newThis, ...args) {
750
- // User is trying to bind() an autobound method; we effectively will
751
- // ignore the value of "this" that the user is trying to use, so
752
- // let's warn.
753
- if (newThis !== component && newThis !== null) {
754
- monitorCodeUse('react_bind_warning', { component: componentName });
755
- console.warn(
756
- 'bind(): React component methods may only be bound to the ' +
757
- 'component instance. See ' + componentName
758
- );
759
- } else if (!args.length) {
760
- monitorCodeUse('react_bind_warning', { component: componentName });
761
- console.warn(
762
- 'bind(): You are binding a component method to the component. ' +
763
- 'React does this for you automatically in a high-performance ' +
764
- 'way, so you can safely remove this call. See ' + componentName
765
- );
766
- return boundMethod;
767
- }
768
- var reboundMethod = _bind.apply(boundMethod, arguments);
769
- reboundMethod.__reactBoundContext = component;
770
- reboundMethod.__reactBoundMethod = method;
771
- reboundMethod.__reactBoundArguments = args;
772
- return reboundMethod;
773
- };
774
- }
775
- return boundMethod;
776
- }
777
-};
717
+ // Stub
718
+ _instantiateReactComponent: null
719
779
-var ReactCompositeComponentBase = function() {};
780
-assign(
781
- ReactCompositeComponentBase.prototype,
782
- ReactComponent.Mixin,
783
- ReactOwner.Mixin,
784
- ReactPropTransferer.Mixin,
785
- ReactCompositeComponentMixin
786
-);
720
+});
721
788
-/**
789
- * Module for creating composite components.
790
- *
791
- * @class ReactCompositeComponent
792
- * @extends ReactComponent
793
- * @extends ReactOwner
794
- * @extends ReactPropTransferer
795
- */
722
var ReactCompositeComponent = {
723
724
LifeCycle: CompositeLifeCycle,
725
800
- Base: ReactCompositeComponentBase
726
+ Mixin: ReactCompositeComponentMixin
727
728
};
729
804
-// Temporary injection.
805
-// TODO: Delete this hack once implementation details are hidden.
806
-instantiateReactComponent._compositeBase = ReactCompositeComponentBase;
807
-
730
module.exports = ReactCompositeComponent;
src/core/ReactElementValidator.js
+5
-2
@@ -48,7 +48,9 @@ var NUMERIC_PROPERTY_REGEX = /^\d+$/;
48
*/
49
function getCurrentOwnerDisplayName() {
50
var current = ReactCurrentOwner.current;
51
- return current && current.constructor.displayName || undefined;
51
+ return (
52
+ current && current.getPublicInstance().constructor.displayName || undefined
53
+ );
54
}
55
56
/**
@@ -128,7 +130,8 @@ function warnAndMonitorForKeyUse(warningID, message, component, parentType) {
130
component._owner &&
131
component._owner !== ReactCurrentOwner.current) {
132
// Name of the component that originally created this child.
131
- childOwnerName = component._owner.constructor.displayName;
133
+ childOwnerName =
134
+ component._owner.getPublicInstance().constructor.displayName;
135
136
message += ` It was passed a child from ${childOwnerName}.`;
137
}
src/core/ReactInstanceMap.js
new
+47
@@ -0,0 +1,47 @@
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 ReactInstanceMap
10
+ */
11
+
12
+"use strict";
13
+
14
+/**
15
+ * `ReactInstanceMap` maintains a mapping from a public facing stateful
16
+ * instance (key) and the internal representation (value). This allows public
17
+ * methods to accept the user facing instance as an argument and map them back
18
+ * to internal methods.
19
+ */
20
+
21
+// TODO: Replace this with ES6: var ReactInstanceMap = new Map();
22
+var ReactInstanceMap = {
23
+
24
+ /**
25
+ * This API should be called `delete` but we'd have to make sure to always
26
+ * transform these to strings for IE support. When this transform is fully
27
+ * supported we can rename it.
28
+ */
29
+ remove: function(key) {
30
+ key._reactInternalInstance = undefined;
31
+ },
32
+
33
+ get: function(key) {
34
+ return key._reactInternalInstance;
35
+ },
36
+
37
+ has: function(key) {
38
+ return key._reactInternalInstance !== undefined;
39
+ },
40
+
41
+ set: function(key, value) {
42
+ key._reactInternalInstance = value;
43
+ }
44
+
45
+};
46
+
47
+module.exports = ReactInstanceMap;
src/core/ReactOwner.js
+10
-5
@@ -100,7 +100,7 @@ var ReactOwner = {
100
);
101
// Check that `component` is still the current ref because we do not want to
102
// detach the ref if another component stole it.
103
- if (owner.refs[ref] === component) {
103
+ if (owner.getPublicInstance().refs[ref] === component.getPublicInstance()) {
104
owner.detachRef(ref);
105
}
106
},
@@ -113,7 +113,8 @@ var ReactOwner = {
113
Mixin: {
114
115
construct: function() {
116
- this.refs = emptyObject;
116
+ var inst = this.getPublicInstance();
117
+ inst.refs = emptyObject;
118
},
119
120
/**
@@ -125,13 +126,16 @@ var ReactOwner = {
126
* @private
127
*/
128
attachRef: function(ref, component) {
129
+ // TODO: Remove this invariant. This is never exposed and cannot be called
130
+ // by user code. The unit test is already removed.
131
invariant(
132
component.isOwnedBy(this),
133
'attachRef(%s, ...): Only a component\'s owner can store a ref to it.',
134
ref
135
);
133
- var refs = this.refs === emptyObject ? (this.refs = {}) : this.refs;
134
- refs[ref] = component;
136
+ var inst = this.getPublicInstance();
137
+ var refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs;
138
+ refs[ref] = component.getPublicInstance();
139
},
140
141
/**
@@ -142,7 +146,8 @@ var ReactOwner = {
146
* @private
147
*/
148
detachRef: function(ref) {
145
- delete this.refs[ref];
149
+ var refs = this.getPublicInstance().refs;
150
+ delete refs[ref];
151
}
152
153
}
src/core/ReactPropTransferer.js
+1
-1
@@ -129,7 +129,7 @@ var ReactPropTransferer = {
129
*/
130
transferPropsTo: function(element) {
131
invariant(
132
- element._owner === this,
132
+ element._owner && element._owner.getPublicInstance() === this,
133
'%s: You can\'t call transferPropsTo() on a component that you ' +
134
'don\'t own, %s. This usually means you are calling ' +
135
'transferPropsTo() on a component passed in as props or children.',
src/core/ReactRef.js
+1
-1
@@ -89,7 +89,7 @@ assign(ReactRef.prototype, {
89
});
90
91
ReactRef.attachRef = function(ref, value) {
92
- ref._value = value;
92
+ ref._value = value.getPublicInstance();
93
};
94
95
ReactRef.detachRef = function(ref, value) {
src/core/__tests__/ReactComponent-test.js
+21
-30
@@ -12,15 +12,26 @@
12
"use strict";
13
14
var React;
15
+var ReactInstanceMap;
16
var ReactTestUtils;
17
18
var reactComponentExpect;
19
+var getMountDepth;
20
21
describe('ReactComponent', function() {
22
beforeEach(function() {
23
React = require('React');
24
+ ReactInstanceMap = require('ReactInstanceMap');
25
ReactTestUtils = require('ReactTestUtils');
26
reactComponentExpect = require('reactComponentExpect');
27
+
28
+ getMountDepth = function(instance) {
29
+ if (instance.mountComponent) {
30
+ // Native instance
31
+ return instance._mountDepth;
32
+ }
33
+ return ReactInstanceMap.get(instance)._mountDepth;
34
+ };
35
});
36
37
it('should throw on invalid render targets', function() {
@@ -48,26 +59,6 @@ describe('ReactComponent', function() {
59
}).toThrow();
60
});
61
51
- it('should throw when attempting to hijack a ref', function() {
52
- var Component = React.createClass({
53
- render: function() {
54
- var child = this.props.child;
55
- this.attachRef('test', child);
56
- return child;
57
- }
58
- });
59
-
60
- var childInstance = ReactTestUtils.renderIntoDocument(<span />);
61
- var instance = <Component child={childInstance} />;
62
-
63
- expect(function() {
64
- instance = ReactTestUtils.renderIntoDocument(instance);
65
- }).toThrow(
66
- 'Invariant Violation: attachRef(test, ...): Only a component\'s owner ' +
67
- 'can store a ref to it.'
68
- );
69
- });
70
-
62
it('should support refs on owned components', function() {
63
var innerObj = {}, outerObj = {};
64
@@ -260,8 +251,8 @@ describe('ReactComponent', function() {
251
252
var instance = <Owner />;
253
instance = ReactTestUtils.renderIntoDocument(instance);
263
- expect(instance._mountDepth).toBe(0);
264
- expect(instance.refs.child._mountDepth).toBe(1);
254
+ expect(getMountDepth(instance)).toBe(0);
255
+ expect(getMountDepth(instance.refs.child)).toBe(1);
256
});
257
258
it('should know its (complicated) mount depth', function() {
@@ -291,7 +282,7 @@ describe('ReactComponent', function() {
282
ref="switcherDiv"
283
style={{
284
display: this.state.tabKey === child.key ? '' : 'none'
294
- }}>
285
+ }}>
286
{child}
287
</div>
288
</Box>
@@ -312,12 +303,12 @@ describe('ReactComponent', function() {
303
var root = <App />;
304
root = ReactTestUtils.renderIntoDocument(root);
305
315
- expect(root._mountDepth).toBe(0);
316
- expect(root.refs.switcher._mountDepth).toBe(1);
317
- expect(root.refs.switcher.refs.box._mountDepth).toBe(2);
318
- expect(root.refs.switcher.refs.switcherDiv._mountDepth).toBe(4);
319
- expect(root.refs.child._mountDepth).toBe(5);
320
- expect(root.refs.switcher.refs.box.refs.boxDiv._mountDepth).toBe(3);
321
- expect(root.refs.child.refs.span._mountDepth).toBe(6);
306
+ expect(getMountDepth(root)).toBe(0);
307
+ expect(getMountDepth(root.refs.switcher)).toBe(1);
308
+ expect(getMountDepth(root.refs.switcher.refs.box)).toBe(2);
309
+ expect(getMountDepth(root.refs.switcher.refs.switcherDiv)).toBe(4);
310
+ expect(getMountDepth(root.refs.child)).toBe(5);
311
+ expect(getMountDepth(root.refs.switcher.refs.box.refs.boxDiv)).toBe(3);
312
+ expect(getMountDepth(root.refs.child.refs.span)).toBe(6);
313
});
314
});
src/core/__tests__/ReactComponentLifeCycle-test.js
+40
-18
@@ -15,9 +15,13 @@ var React;
15
var ReactTestUtils;
16
var ReactComponent;
17
var ReactCompositeComponent;
18
+var ReactInstanceMap;
19
var ComponentLifeCycle;
20
var CompositeComponentLifeCycle;
21
22
+var getCompositeLifeCycle;
23
+var getLifeCycleState;
24
+
25
var clone = function(o) {
26
return JSON.parse(JSON.stringify(o));
27
};
@@ -81,6 +85,23 @@ describe('ReactComponentLifeCycle', function() {
85
ReactCompositeComponent = require('ReactCompositeComponent');
86
ComponentLifeCycle = ReactComponent.LifeCycle;
87
CompositeComponentLifeCycle = ReactCompositeComponent.LifeCycle;
88
+
89
+ ReactInstanceMap = require('ReactInstanceMap');
90
+
91
+ getCompositeLifeCycle = function(instance) {
92
+ return ReactInstanceMap.get(instance)._compositeLifeCycleState;
93
+ };
94
+
95
+ getLifeCycleState = function(instance) {
96
+ var internalInstance = ReactInstanceMap.get(instance);
97
+ if (!internalInstance) {
98
+ // Once a component is fully unmounted, we cannot actually get to the
99
+ // internal instance. It's already dereferenced and possibly GC:ed.
100
+ // So the unmounted life cycle hook doesn't exist anymore.
101
+ return ComponentLifeCycle.UNMOUNTED;
102
+ }
103
+ return internalInstance._lifeCycleState;
104
+ };
105
});
106
107
it('should not reuse an instance when it has been unmounted', function() {
@@ -222,25 +243,25 @@ describe('ReactComponentLifeCycle', function() {
243
};
244
this._testJournal.returnedFromGetInitialState = clone(initState);
245
this._testJournal.lifeCycleAtStartOfGetInitialState =
225
- this._lifeCycleState;
246
+ getLifeCycleState(this);
247
this._testJournal.compositeLifeCycleAtStartOfGetInitialState =
227
- this._compositeLifeCycleState;
248
+ getCompositeLifeCycle(this);
249
return initState;
250
},
251
252
componentWillMount: function() {
253
this._testJournal.stateAtStartOfWillMount = clone(this.state);
254
this._testJournal.lifeCycleAtStartOfWillMount =
234
- this._lifeCycleState;
255
+ getLifeCycleState(this);
256
this._testJournal.compositeLifeCycleAtStartOfWillMount =
236
- this._compositeLifeCycleState;
257
+ getCompositeLifeCycle(this);
258
this.state.hasWillMountCompleted = true;
259
},
260
261
componentDidMount: function() {
262
this._testJournal.stateAtStartOfDidMount = clone(this.state);
263
this._testJournal.lifeCycleAtStartOfDidMount =
243
- this._lifeCycleState;
264
+ getLifeCycleState(this);
265
this.setState({hasDidMountCompleted: true});
266
},
267
@@ -248,12 +269,12 @@ describe('ReactComponentLifeCycle', function() {
269
var isInitialRender = !this.state.hasRenderCompleted;
270
if (isInitialRender) {
271
this._testJournal.stateInInitialRender = clone(this.state);
251
- this._testJournal.lifeCycleInInitialRender = this._lifeCycleState;
272
+ this._testJournal.lifeCycleInInitialRender = getLifeCycleState(this);
273
this._testJournal.compositeLifeCycleInInitialRender =
253
- this._compositeLifeCycleState;
274
+ getCompositeLifeCycle(this);
275
} else {
276
this._testJournal.stateInLaterRender = clone(this.state);
256
- this._testJournal.lifeCycleInLaterRender = this._lifeCycleState;
277
+ this._testJournal.lifeCycleInLaterRender = getLifeCycleState(this);
278
}
279
// you would *NEVER* do anything like this in real code!
280
this.state.hasRenderCompleted = true;
@@ -267,7 +288,7 @@ describe('ReactComponentLifeCycle', function() {
288
componentWillUnmount: function() {
289
this._testJournal.stateAtStartOfWillUnmount = clone(this.state);
290
this._testJournal.lifeCycleAtStartOfWillUnmount =
270
- this._lifeCycleState;
291
+ getLifeCycleState(this);
292
this.state.hasWillUnmountCompleted = true;
293
}
294
});
@@ -275,7 +296,8 @@ describe('ReactComponentLifeCycle', function() {
296
// A component that is merely "constructed" (as in "constructor") but not
297
// yet initialized, or rendered.
298
//
278
- var instance = ReactTestUtils.renderIntoDocument(<LifeCycleComponent />);
299
+ var container = document.createElement('div');
300
+ var instance = React.render(<LifeCycleComponent />, container);
301
302
// getInitialState
303
expect(instance._testJournal.returnedFromGetInitialState).toEqual(
@@ -312,7 +334,7 @@ describe('ReactComponentLifeCycle', function() {
334
CompositeComponentLifeCycle.MOUNTING
335
);
336
315
- expect(instance._lifeCycleState).toBe(ComponentLifeCycle.MOUNTED);
337
+ expect(getLifeCycleState(instance)).toBe(ComponentLifeCycle.MOUNTED);
338
339
// Now *update the component*
340
instance.forceUpdate();
@@ -324,10 +346,9 @@ describe('ReactComponentLifeCycle', function() {
346
ComponentLifeCycle.MOUNTED
347
);
348
327
- expect(instance._lifeCycleState).toBe(ComponentLifeCycle.MOUNTED);
349
+ expect(getLifeCycleState(instance)).toBe(ComponentLifeCycle.MOUNTED);
350
329
- // Now *unmountComponent*
330
- instance.unmountComponent();
351
+ React.unmountComponentAtNode(container);
352
353
expect(instance._testJournal.stateAtStartOfWillUnmount)
354
.toEqual(WILL_UNMOUNT_STATE);
@@ -337,7 +358,7 @@ describe('ReactComponentLifeCycle', function() {
358
);
359
360
// But the current lifecycle of the component is unmounted.
340
- expect(instance._lifeCycleState).toBe(ComponentLifeCycle.UNMOUNTED);
361
+ expect(getLifeCycleState(instance)).toBe(ComponentLifeCycle.UNMOUNTED);
362
expect(instance.state).toEqual(POST_WILL_UNMOUNT_STATE);
363
});
364
@@ -487,10 +508,11 @@ describe('ReactComponentLifeCycle', function() {
508
componentDidUpdate: logger('inner componentDidUpdate'),
509
componentWillUnmount: logger('inner componentWillUnmount')
510
});
490
- var instance;
511
512
+
513
+ var container = document.createElement('div');
514
log = [];
493
- instance = ReactTestUtils.renderIntoDocument(<Outer x={17} />);
515
+ var instance = React.render(<Outer x={17} />, container);
516
expect(log).toEqual([
517
'outer componentWillMount',
518
'inner componentWillMount',
@@ -512,7 +534,7 @@ describe('ReactComponentLifeCycle', function() {
534
]);
535
536
log = [];
515
- instance.unmountComponent();
537
+ React.unmountComponentAtNode(container);
538
expect(log).toEqual([
539
'outer componentWillUnmount',
540
'inner componentWillUnmount'
src/core/__tests__/ReactCompositeComponent-test.js
+57
@@ -623,6 +623,63 @@ describe('ReactCompositeComponent', function() {
623
);
624
});
625
626
+ it('should not allow `setState` on unmounted components', function() {
627
+ var container = document.createElement('div');
628
+ document.documentElement.appendChild(container);
629
+
630
+ var Component = React.createClass({
631
+ getInitialState: function() {
632
+ return { value: 0 };
633
+ },
634
+ render: function() {
635
+ return <div />;
636
+ }
637
+ });
638
+
639
+ var instance = <Component />;
640
+ expect(instance.setState).not.toBeDefined();
641
+
642
+ instance = React.render(instance, container);
643
+ expect(function() {
644
+ instance.setState({ value: 1 });
645
+ }).not.toThrow();
646
+
647
+ React.unmountComponentAtNode(container);
648
+ expect(function() {
649
+ instance.setState({ value: 2 });
650
+ }).toThrow(
651
+ 'Invariant Violation: setState(...): Can only update a mounted or ' +
652
+ 'mounting component.'
653
+ );
654
+ });
655
+
656
+ it('should not allow `setProps` on unmounted components', function() {
657
+ var container = document.createElement('div');
658
+ document.documentElement.appendChild(container);
659
+
660
+ var Component = React.createClass({
661
+ render: function() {
662
+ return <div />;
663
+ }
664
+ });
665
+
666
+ var instance = <Component />;
667
+ expect(instance.setProps).not.toBeDefined();
668
+
669
+ instance = React.render(instance, container);
670
+ expect(function() {
671
+ instance.setProps({ value: 1 });
672
+ }).not.toThrow();
673
+
674
+ React.unmountComponentAtNode(container);
675
+ expect(function() {
676
+ instance.setProps({ value: 2 });
677
+ }).toThrow(
678
+ 'Invariant Violation: setProps(...): Can only update a mounted ' +
679
+ 'component.'
680
+ );
681
+ });
682
+
683
it('should cleanup even if render() fatals', function() {
684
var BadComponent = React.createClass({
685
render: function() {
src/core/__tests__/ReactCompositeComponentState-test.js
+12
-3
@@ -14,6 +14,7 @@
14
var mocks = require('mocks');
15
16
var React;
17
+var ReactInstanceMap;
18
var ReactTestUtils;
19
var reactComponentExpect;
20
@@ -23,6 +24,7 @@ describe('ReactCompositeComponent-state', function() {
24
25
beforeEach(function() {
26
React = require('React');
27
+ ReactInstanceMap = require('ReactInstanceMap');
28
ReactTestUtils = require('ReactTestUtils');
29
reactComponentExpect = require('reactComponentExpect');
30
@@ -31,10 +33,13 @@ describe('ReactCompositeComponent-state', function() {
33
if (state) {
34
this.props.stateListener(from, state && state.color);
35
} else {
36
+ var internalInstance = ReactInstanceMap.get(this);
37
+ var pendingState = internalInstance ? internalInstance._pendingState :
38
+ null;
39
this.props.stateListener(
40
from,
41
this.state && this.state.color,
37
- this._pendingState && this._pendingState.color
42
+ pendingState && pendingState.color
43
);
44
}
45
},
@@ -99,13 +104,17 @@ describe('ReactCompositeComponent-state', function() {
104
});
105
106
it('should support setting state', function() {
107
+ var container = document.createElement('div');
108
+ document.documentElement.appendChild(container);
109
+
110
var stateListener = mocks.getMockFunction();
111
var instance = <TestComponent stateListener={stateListener} />;
104
- instance = ReactTestUtils.renderIntoDocument(instance);
112
+ instance = React.render(instance, container);
113
instance.setProps({nextColor: 'green'});
114
instance.setFavoriteColor('blue');
115
instance.forceUpdate();
108
- instance.unmountComponent();
116
+
117
+ React.unmountComponentAtNode(container);
118
119
expect(stateListener.mock.calls).toEqual([
120
// there is no state when getInitialState() is called
src/core/__tests__/ReactElement-test.js
+1
-1
@@ -138,7 +138,7 @@ describe('ReactElement', function() {
138
139
var instance = ReactTestUtils.renderIntoDocument(<Wrapper />);
140
141
- expect(element._owner).toBe(instance);
141
+ expect(element._owner.getPublicInstance()).toBe(instance);
142
});
143
144
it('merges an additional argument onto the children prop', function() {
src/core/__tests__/ReactInstanceHandles-test.js
+58
-50
@@ -62,6 +62,14 @@ describe('ReactInstanceHandles', function() {
62
});
63
}
64
65
+ function getNodeID(instance) {
66
+ if (instance === null) {
67
+ return '';
68
+ }
69
+ var internal = ReactTestUtils.getInternalRepresentation(instance);
70
+ return internal._rootNodeID;
71
+ }
72
+
73
beforeEach(function() {
74
ReactInstanceHandles = require('ReactInstanceHandles');
75
aggregatedArgs = [];
@@ -178,17 +186,17 @@ describe('ReactInstanceHandles', function() {
186
187
it("should traverse two phase across component boundary", function() {
188
var parent = renderParentIntoDocument();
181
- var targetID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
189
+ var targetID = getNodeID(parent.refs.P_P1_C1.refs.DIV_1);
190
var expectedAggregation = [
183
- {id: parent.refs.P._rootNodeID, isUp: false, arg: ARG},
184
- {id: parent.refs.P_P1._rootNodeID, isUp: false, arg: ARG},
185
- {id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: false, arg: ARG},
186
- {id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: false, arg: ARG},
187
-
188
- {id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: true, arg: ARG},
189
- {id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: true, arg: ARG},
190
- {id: parent.refs.P_P1._rootNodeID, isUp: true, arg: ARG},
191
- {id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
191
+ {id: getNodeID(parent.refs.P), isUp: false, arg: ARG},
192
+ {id: getNodeID(parent.refs.P_P1), isUp: false, arg: ARG},
193
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: false, arg: ARG},
194
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV_1), isUp: false, arg: ARG},
195
+
196
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV_1), isUp: true, arg: ARG},
197
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: true, arg: ARG},
198
+ {id: getNodeID(parent.refs.P_P1), isUp: true, arg: ARG},
199
+ {id: getNodeID(parent.refs.P), isUp: true, arg: ARG}
200
];
201
ReactInstanceHandles.traverseTwoPhase(targetID, argAggregator, ARG);
202
expect(aggregatedArgs).toEqual(expectedAggregation);
@@ -196,10 +204,10 @@ describe('ReactInstanceHandles', function() {
204
205
it("should traverse two phase at shallowest node", function() {
206
var parent = renderParentIntoDocument();
199
- var targetID = parent.refs.P._rootNodeID;
207
+ var targetID = getNodeID(parent.refs.P);
208
var expectedAggregation = [
201
- {id: parent.refs.P._rootNodeID, isUp: false, arg: ARG},
202
- {id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
209
+ {id: getNodeID(parent.refs.P), isUp: false, arg: ARG},
210
+ {id: getNodeID(parent.refs.P), isUp: true, arg: ARG}
211
];
212
ReactInstanceHandles.traverseTwoPhase(targetID, argAggregator, ARG);
213
expect(aggregatedArgs).toEqual(expectedAggregation);
@@ -218,8 +226,8 @@ describe('ReactInstanceHandles', function() {
226
227
it("should not traverse if enter/leave the same node", function() {
228
var parent = renderParentIntoDocument();
221
- var leaveID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
222
- var enterID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
229
+ var leaveID = getNodeID(parent.refs.P_P1_C1.refs.DIV_1);
230
+ var enterID = getNodeID(parent.refs.P_P1_C1.refs.DIV_1);
231
var expectedAggregation = [];
232
ReactInstanceHandles.traverseEnterLeave(
233
leaveID, enterID, argAggregator, ARG, ARG2
@@ -229,12 +237,12 @@ describe('ReactInstanceHandles', function() {
237
238
it("should traverse enter/leave to sibling - avoids parent", function() {
239
var parent = renderParentIntoDocument();
232
- var leaveID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
233
- var enterID = parent.refs.P_P1_C1.refs.DIV_2._rootNodeID;
240
+ var leaveID = getNodeID(parent.refs.P_P1_C1.refs.DIV_1);
241
+ var enterID = getNodeID(parent.refs.P_P1_C1.refs.DIV_2);
242
var expectedAggregation = [
235
- {id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: true, arg: ARG},
243
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV_1), isUp: true, arg: ARG},
244
// enter/leave shouldn't fire antyhing on the parent
237
- {id: parent.refs.P_P1_C1.refs.DIV_2._rootNodeID, isUp: false, arg: ARG2}
245
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV_2), isUp: false, arg: ARG2}
246
];
247
ReactInstanceHandles.traverseEnterLeave(
248
leaveID, enterID, argAggregator, ARG, ARG2
@@ -244,10 +252,10 @@ describe('ReactInstanceHandles', function() {
252
253
it("should traverse enter/leave to parent - avoids parent", function() {
254
var parent = renderParentIntoDocument();
247
- var leaveID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
248
- var enterID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
255
+ var leaveID = getNodeID(parent.refs.P_P1_C1.refs.DIV_1);
256
+ var enterID = getNodeID(parent.refs.P_P1_C1.refs.DIV);
257
var expectedAggregation = [
250
- {id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: true, arg: ARG}
258
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV_1), isUp: true, arg: ARG}
259
];
260
ReactInstanceHandles.traverseEnterLeave(
261
leaveID, enterID, argAggregator, ARG, ARG2
@@ -258,11 +266,11 @@ describe('ReactInstanceHandles', function() {
266
it("should enter from the window", function() {
267
var parent = renderParentIntoDocument();
268
var leaveID = ''; // From the window or outside of the React sandbox.
261
- var enterID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
269
+ var enterID = getNodeID(parent.refs.P_P1_C1.refs.DIV);
270
var expectedAggregation = [
263
- {id: parent.refs.P._rootNodeID, isUp: false, arg: ARG2},
264
- {id: parent.refs.P_P1._rootNodeID, isUp: false, arg: ARG2},
265
- {id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: false, arg: ARG2}
271
+ {id: getNodeID(parent.refs.P), isUp: false, arg: ARG2},
272
+ {id: getNodeID(parent.refs.P_P1), isUp: false, arg: ARG2},
273
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: false, arg: ARG2}
274
];
275
ReactInstanceHandles.traverseEnterLeave(
276
leaveID, enterID, argAggregator, ARG, ARG2
@@ -273,9 +281,9 @@ describe('ReactInstanceHandles', function() {
281
it("should enter from the window to the shallowest", function() {
282
var parent = renderParentIntoDocument();
283
var leaveID = ''; // From the window or outside of the React sandbox.
276
- var enterID = parent.refs.P._rootNodeID;
284
+ var enterID = getNodeID(parent.refs.P);
285
var expectedAggregation = [
278
- {id: parent.refs.P._rootNodeID, isUp: false, arg: ARG2}
286
+ {id: getNodeID(parent.refs.P), isUp: false, arg: ARG2}
287
];
288
ReactInstanceHandles.traverseEnterLeave(
289
leaveID, enterID, argAggregator, ARG, ARG2
@@ -286,11 +294,11 @@ describe('ReactInstanceHandles', function() {
294
it("should leave to the window", function() {
295
var parent = renderParentIntoDocument();
296
var enterID = ''; // From the window or outside of the React sandbox.
289
- var leaveID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
297
+ var leaveID = getNodeID(parent.refs.P_P1_C1.refs.DIV);
298
var expectedAggregation = [
291
- {id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: true, arg: ARG},
292
- {id: parent.refs.P_P1._rootNodeID, isUp: true, arg: ARG},
293
- {id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
299
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: true, arg: ARG},
300
+ {id: getNodeID(parent.refs.P_P1), isUp: true, arg: ARG},
301
+ {id: getNodeID(parent.refs.P), isUp: true, arg: ARG}
302
];
303
ReactInstanceHandles.traverseEnterLeave(
304
leaveID, enterID, argAggregator, ARG, ARG2
@@ -301,11 +309,11 @@ describe('ReactInstanceHandles', function() {
309
it("should leave to the window from the shallowest", function() {
310
var parent = renderParentIntoDocument();
311
var enterID = ''; // From the window or outside of the React sandbox.
304
- var leaveID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
312
+ var leaveID = getNodeID(parent.refs.P_P1_C1.refs.DIV);
313
var expectedAggregation = [
306
- {id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: true, arg: ARG},
307
- {id: parent.refs.P_P1._rootNodeID, isUp: true, arg: ARG},
308
- {id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
314
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: true, arg: ARG},
315
+ {id: getNodeID(parent.refs.P_P1), isUp: true, arg: ARG},
316
+ {id: getNodeID(parent.refs.P), isUp: true, arg: ARG}
317
];
318
ReactInstanceHandles.traverseEnterLeave(
319
leaveID, enterID, argAggregator, ARG, ARG2
@@ -320,9 +328,9 @@ describe('ReactInstanceHandles', function() {
328
expect(
329
ReactInstanceHandles._getNextDescendantID(
330
'',
323
- parent.refs.P_P1._rootNodeID
331
+ getNodeID(parent.refs.P_P1)
332
)
325
- ).toBe(parent.refs.P._rootNodeID);
333
+ ).toBe(getNodeID(parent.refs.P));
334
});
335
336
it("should return window for next descendent towards window", function() {
@@ -333,10 +341,10 @@ describe('ReactInstanceHandles', function() {
341
var parent = renderParentIntoDocument();
342
expect(
343
ReactInstanceHandles._getNextDescendantID(
336
- parent.refs.P_P1._rootNodeID,
337
- parent.refs.P_P1._rootNodeID
344
+ getNodeID(parent.refs.P_P1),
345
+ getNodeID(parent.refs.P_P1)
346
)
339
- ).toBe(parent.refs.P_P1._rootNodeID);
347
+ ).toBe(getNodeID(parent.refs.P_P1));
348
});
349
});
350
@@ -345,19 +353,19 @@ describe('ReactInstanceHandles', function() {
353
var parent = renderParentIntoDocument();
354
var ancestors = [
355
// Common ancestor from window to deep element is ''.
348
- { one: {_rootNodeID: ''},
356
+ { one: null,
357
two: parent.refs.P_P1_C1.refs.DIV_1,
350
- com: {_rootNodeID: ''}
358
+ com: null
359
},
360
// Same as previous - reversed direction.
361
{ one: parent.refs.P_P1_C1.refs.DIV_1,
354
- two: {_rootNodeID: ''},
355
- com: {_rootNodeID: ''}
362
+ two: null,
363
+ com: null
364
},
365
// Common ancestor from window to shallow id is ''.
366
{ one: parent.refs.P,
359
- two: {_rootNodeID: ''},
360
- com: {_rootNodeID: ''}
367
+ two: null,
368
+ com: null
369
},
370
// Common ancestor with self is self.
371
{ one: parent.refs.P_P1_C1.refs.DIV_1,
@@ -401,10 +409,10 @@ describe('ReactInstanceHandles', function() {
409
for (i = 0; i < ancestors.length; i++) {
410
var plan = ancestors[i];
411
var firstCommon = ReactInstanceHandles._getFirstCommonAncestorID(
404
- plan.one._rootNodeID,
405
- plan.two._rootNodeID
412
+ getNodeID(plan.one),
413
+ getNodeID(plan.two)
414
);
407
- expect(firstCommon).toBe(plan.com._rootNodeID);
415
+ expect(firstCommon).toBe(getNodeID(plan.com));
416
}
417
});
418
});
src/core/__tests__/ReactMultiChildReconcile-test.js
+6
-2
@@ -14,6 +14,7 @@
14
require('mock-modules');
15
16
var React = require('React');
17
+var ReactInstanceMap = require('ReactInstanceMap');
18
var ReactTestUtils = require('ReactTestUtils');
19
var ReactMount = require('ReactMount');
20
@@ -83,7 +84,10 @@ var FriendsStatusDisplay = React.createClass({
84
getStatusDisplays: function() {
85
var name;
86
var orderOfUsernames = [];
86
- var statusDisplays = this._renderedComponent._renderedChildren;
87
+ // TODO: Update this to a better test that doesn't rely so much on internal
88
+ // implementation details.
89
+ var statusDisplays =
90
+ ReactInstanceMap.get(this)._renderedComponent._renderedChildren;
91
for (name in statusDisplays) {
92
var child = statusDisplays[name];
93
var isPresent = !!child;
@@ -195,7 +199,7 @@ function verifyDomOrderingAccurate(parentInstance, statusDisplays) {
199
continue;
200
}
201
var statusDisplay = statusDisplays[username];
198
- orderedLogicalIds.push(statusDisplay._rootNodeID);
202
+ orderedLogicalIds.push(ReactInstanceMap.get(statusDisplay)._rootNodeID);
203
}
204
expect(orderedDomIds).toEqual(orderedLogicalIds);
205
}
src/core/__tests__/refs-destruction-test.js
+11
-7
@@ -19,9 +19,11 @@ var TestComponent = React.createClass({
19
render: function() {
20
return (
21
<div>
22
- <div ref="theInnerDiv">
23
- Lets try to destroy this.
24
- </div>
22
+ {this.props.destroy ? null :
23
+ <div ref="theInnerDiv">
24
+ Lets try to destroy this.
25
+ </div>
26
+ }
27
</div>
28
);
29
}
@@ -33,20 +35,22 @@ describe('refs-destruction', function() {
35
});
36
37
it("should remove refs when destroying the parent", function() {
36
- var testInstance = ReactTestUtils.renderIntoDocument(<TestComponent />);
38
+ var container = document.createElement('div');
39
+ var testInstance = React.render(<TestComponent />, container);
40
reactComponentExpect(testInstance.refs.theInnerDiv)
41
.toBeDOMComponentWithTag('div');
42
expect(Object.keys(testInstance.refs || {}).length).toEqual(1);
40
- testInstance.unmountComponent();
43
+ React.unmountComponentAtNode(container);
44
expect(Object.keys(testInstance.refs || {}).length).toEqual(0);
45
});
46
47
it("should remove refs when destroying the child", function() {
45
- var testInstance = ReactTestUtils.renderIntoDocument(<TestComponent />);
48
+ var container = document.createElement('div');
49
+ var testInstance = React.render(<TestComponent />, container);
50
reactComponentExpect(testInstance.refs.theInnerDiv)
51
.toBeDOMComponentWithTag('div');
52
expect(Object.keys(testInstance.refs || {}).length).toEqual(1);
49
- testInstance.refs.theInnerDiv.unmountComponent();
53
+ React.render(<TestComponent destroy={true} />, container);
54
expect(Object.keys(testInstance.refs || {}).length).toEqual(0);
55
});
56
});
src/core/instantiateReactComponent.js
+44
-30
@@ -12,30 +12,37 @@
12
13
"use strict";
14
15
-var warning = require('warning');
16
-
15
+var ReactCompositeComponent = require('ReactCompositeComponent');
16
var ReactNativeComponent = require('ReactNativeComponent');
17
19
-// This is temporary until we've hidden all the implementation details
20
-// TODO: Delete this hack once implementation details are hidden
21
-var publicAPIs = {
22
- forceUpdate: true,
23
- replaceState: true,
24
- setProps: true,
25
- setState: true,
26
- getDOMNode: true
27
- // Public APIs used internally:
28
- // isMounted: true,
29
- // replaceProps: true,
30
-};
18
+var assign = require('Object.assign');
19
+var warning = require('warning');
20
32
-function unmockImplementationDetails(mockInstance) {
33
- var ReactCompositeComponentBase = instantiateReactComponent._compositeBase;
34
- for (var key in ReactCompositeComponentBase.prototype) {
35
- if (!publicAPIs.hasOwnProperty(key)) {
36
- mockInstance[key] = ReactCompositeComponentBase.prototype[key];
37
- }
21
+// To avoid a cyclic dependency, we create the final class in this module
22
+var ReactCompositeComponentWrapper = function(inst) {
23
+ this._instance = inst;
24
+};
25
+assign(
26
+ ReactCompositeComponentWrapper.prototype,
27
+ ReactCompositeComponent.Mixin,
28
+ {
29
+ _instantiateReactComponent: instantiateReactComponent
30
}
31
+);
32
+
33
+/**
34
+ * Check if the type reference is a known internal type. I.e. not a user
35
+ * provided composite type.
36
+ *
37
+ * @param {function} type
38
+ * @return {boolean} Returns true if this is a valid internal type.
39
+ */
40
+function isInternalComponentType(type) {
41
+ return (
42
+ typeof type === 'function' &&
43
+ typeof type.prototype.mountComponent === 'function' &&
44
+ typeof type.prototype.receiveComponent === 'function'
45
+ );
46
}
47
48
/**
@@ -52,7 +59,7 @@ function instantiateReactComponent(element, parentCompositeType) {
59
if (__DEV__) {
60
warning(
61
element && (typeof element.type === 'function' ||
55
- typeof element.type === 'string'),
62
+ typeof element.type === 'string'),
63
'Only functions or strings can be mounted as React components.'
64
);
65
}
@@ -64,17 +71,25 @@ function instantiateReactComponent(element, parentCompositeType) {
71
element.props,
72
parentCompositeType
73
);
74
+ // If the injected special class is not an internal class, but another
75
+ // composite, then we must wrap it.
76
+ // TODO: Move this resolution around to something cleaner.
77
+ if (typeof instance.mountComponent !== 'function') {
78
+ instance = new ReactCompositeComponentWrapper(instance);
79
+ }
80
+ } else if (isInternalComponentType(element.type)) {
81
+ // This is temporarily available for custom components that are not string
82
+ // represenations. I.e. ART. Once those are updated to use the string
83
+ // representation, we can drop this code path.
84
+ instance = new element.type(element);
85
} else {
68
- // Normal case for non-mocks and non-strings
69
- instance = new element.type(element.props);
86
+ // TODO: Update to follow new ES6 initialization. Ideally, we can use props
87
+ // in property initializers.
88
+ var inst = new element.type(element.props);
89
+ instance = new ReactCompositeComponentWrapper(inst);
90
}
91
92
if (__DEV__) {
73
- if (element.type._isMockFunction) {
74
- // TODO: Remove this special case
75
- unmockImplementationDetails(instance);
76
- }
77
-
93
warning(
94
typeof instance.construct === 'function' &&
95
typeof instance.mountComponent === 'function' &&
@@ -83,8 +98,7 @@ function instantiateReactComponent(element, parentCompositeType) {
98
);
99
}
100
86
- // This actually sets up the internal instance. This will become decoupled
87
- // from the public instance in a future diff.
101
+ // Sets up the instance. This can probably just move into the constructor now.
102
instance.construct(element);
103
104
return instance;
src/test/ReactTestUtils.js
+28
-3
@@ -17,6 +17,7 @@ var EventPropagators = require('EventPropagators');
17
var React = require('React');
18
var ReactElement = require('ReactElement');
19
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
20
+var ReactInstanceMap = require('ReactInstanceMap');
21
var ReactMount = require('ReactMount');
22
var ReactTextComponent = require('ReactTextComponent');
23
var ReactUpdates = require('ReactUpdates');
@@ -60,7 +61,9 @@ var ReactTestUtils = {
61
},
62
63
isDOMComponent: function(inst) {
63
- return !!(inst && inst.mountComponent && inst.tagName);
64
+ // TODO: Fix this heuristic. It's just here because composites can currently
65
+ // pretend to be DOM components.
66
+ return !!(inst && inst.getDOMNode && inst.tagName);
67
},
68
69
isDOMComponentElement: function(inst) {
@@ -101,6 +104,22 @@ var ReactTestUtils = {
104
return inst instanceof ReactTextComponent.type;
105
},
106
107
+ getInternalRepresentation: function(inst) {
108
+ // TODO: Remove this duck check once we have a separate DOM/Native instance
109
+ if (typeof inst.mountComponent === 'function') {
110
+ return inst;
111
+ }
112
+ return ReactInstanceMap.get(inst);
113
+ },
114
+
115
+ getRenderedChildOfCompositeComponent: function(inst) {
116
+ if (!ReactTestUtils.isCompositeComponent(inst)) {
117
+ return null;
118
+ }
119
+ var internalInstance = ReactInstanceMap.get(inst);
120
+ return internalInstance._renderedComponent.getPublicInstance();
121
+ },
122
+
123
findAllInRenderedTree: function(inst, test) {
124
if (!inst) {
125
return [];
@@ -114,12 +133,18 @@ var ReactTestUtils = {
133
continue;
134
}
135
ret = ret.concat(
117
- ReactTestUtils.findAllInRenderedTree(renderedChildren[key], test)
136
+ ReactTestUtils.findAllInRenderedTree(
137
+ renderedChildren[key].getPublicInstance(),
138
+ test
139
+ )
140
);
141
}
142
} else if (ReactTestUtils.isCompositeComponent(inst)) {
143
ret = ret.concat(
122
- ReactTestUtils.findAllInRenderedTree(inst._renderedComponent, test)
144
+ ReactTestUtils.findAllInRenderedTree(
145
+ ReactTestUtils.getRenderedChildOfCompositeComponent(inst),
146
+ test
147
+ )
148
);
149
}
150
return ret;
src/test/reactComponentExpect.js
+15
-9
@@ -12,6 +12,7 @@
12
13
"use strict";
14
15
+var ReactInstanceMap = require('ReactInstanceMap');
16
var ReactTestUtils = require('ReactTestUtils');
17
18
var assign = require('Object.assign');
@@ -25,7 +26,10 @@ function reactComponentExpect(instance) {
26
return new reactComponentExpect(instance);
27
}
28
28
- this._instance = instance;
29
+ expect(instance).not.toBeNull();
30
+
31
+ this._instance = ReactTestUtils.getInternalRepresentation(instance);
32
+
33
expect(typeof instance).toBe('object');
34
expect(typeof instance.constructor).toBe('function');
35
expect(ReactTestUtils.isElement(instance)).toBe(false);
@@ -38,7 +42,7 @@ assign(reactComponentExpect.prototype, {
42
* @instance: Retrieves the backing instance.
43
*/
44
instance: function() {
41
- return this._instance;
45
+ return this._instance.getPublicInstance();
46
},
47
48
/**
@@ -57,7 +61,8 @@ assign(reactComponentExpect.prototype, {
61
*/
62
expectRenderedChild: function() {
63
this.toBeCompositeComponent();
60
- return new reactComponentExpect(this.instance()._renderedComponent);
64
+ var child = this._instance._renderedComponent;
65
+ return new reactComponentExpect(child);
66
},
67
68
/**
@@ -67,7 +72,7 @@ assign(reactComponentExpect.prototype, {
72
// Currently only dom components have arrays of children, but that will
73
// change soon.
74
this.toBeDOMComponent();
70
- var renderedChildren = this.instance()._renderedChildren || {};
75
+ var renderedChildren = this._instance._renderedChildren || {};
76
for (var name in renderedChildren) {
77
if (!renderedChildren.hasOwnProperty(name)) {
78
continue;
@@ -83,15 +88,15 @@ assign(reactComponentExpect.prototype, {
88
89
toBeDOMComponentWithChildCount: function(n) {
90
this.toBeDOMComponent();
86
- expect(this.instance()._renderedChildren).toBeTruthy();
87
- var len = Object.keys(this.instance()._renderedChildren).length;
91
+ expect(this._instance._renderedChildren).toBeTruthy();
92
+ var len = Object.keys(this._instance._renderedChildren).length;
93
expect(len).toBe(n);
94
return this;
95
},
96
97
toBeDOMComponentWithNoChildren: function() {
98
this.toBeDOMComponent();
94
- expect(this.instance()._renderedChildren).toBeFalsy();
99
+ expect(this._instance._renderedChildren).toBeFalsy();
100
return this;
101
},
102
@@ -99,7 +104,7 @@ assign(reactComponentExpect.prototype, {
104
105
toBeComponentOfType: function(constructor) {
106
expect(
102
- this.instance()._currentElement.type === constructor
107
+ this._instance._currentElement.type === constructor
108
).toBe(true);
109
return this;
110
},
@@ -110,6 +115,7 @@ assign(reactComponentExpect.prototype, {
115
*/
116
toBeCompositeComponent: function() {
117
expect(
118
+ typeof this.instance() === 'object' &&
119
typeof this.instance().render === 'function' &&
120
typeof this.instance().setState === 'function'
121
).toBe(true);
@@ -119,7 +125,7 @@ assign(reactComponentExpect.prototype, {
125
toBeCompositeComponentWithType: function(constructor) {
126
this.toBeCompositeComponent();
127
expect(
122
- this.instance()._currentElement.type === constructor
128
+ this._instance._currentElement.type === constructor
129
).toBe(true);
130
return this;
131
},