Add legacy methods to DOM components for compatibility
Ben Alpert committed
Jun 17, 2015 at 20:29 UTC
eefda9377c7b734ceba413a3173cb48a357c04ae
6 files changed
+245
-34
src/renderers/dom/client/ReactMount.js
+1
-5
@@ -274,11 +274,7 @@ function mountComponentIntoNode(
274
var markup = ReactReconciler.mountComponent(
275
componentInstance, rootID, transaction, context
276
);
277
- if (typeof componentInstance._renderedComponent._currentElement.type ===
278
- 'function') {
279
- // hax
280
- componentInstance._renderedComponent._isTopLevel = true;
281
- }
277
+ componentInstance._renderedComponent._topLevelWrapper = componentInstance;
278
ReactMount._mountImageIntoNode(markup, container, shouldReuseMarkup);
279
}
280
src/renderers/dom/shared/ReactDOMComponent.js
+155
-1
@@ -30,6 +30,7 @@ var ReactDOMTextarea = require('ReactDOMTextarea');
30
var ReactMount = require('ReactMount');
31
var ReactMultiChild = require('ReactMultiChild');
32
var ReactPerf = require('ReactPerf');
33
+var ReactUpdateQueue = require('ReactUpdateQueue');
34
35
var assign = require('Object.assign');
36
var escapeTextContentForBrowser = require('escapeTextContentForBrowser');
@@ -51,6 +52,122 @@ var STYLE = keyOf({style: null});
52
53
var ELEMENT_NODE_TYPE = 1;
54
55
+var canDefineProperty = false;
56
+try {
57
+ Object.defineProperty({}, 'test', {get: function() {}});
58
+ canDefineProperty = true;
59
+} catch (e) {
60
+}
61
+
62
+function getDeclarationErrorAddendum(internalInstance) {
63
+ if (internalInstance) {
64
+ var owner = internalInstance._currentElement._owner || null;
65
+ if (owner) {
66
+ var name = owner.getName();
67
+ if (name) {
68
+ return ' This DOM node was rendered by `' + name + '`.';
69
+ }
70
+ }
71
+ }
72
+ return '';
73
+}
74
+
75
+var legacyPropsDescriptor;
76
+if (__DEV__) {
77
+ legacyPropsDescriptor = {
78
+ props: {
79
+ enumerable: false,
80
+ get: function() {
81
+ var component = this._reactInternalComponent;
82
+ warning(
83
+ false,
84
+ 'ReactDOMComponent: Do not access .props of a DOM node; instead, ' +
85
+ 'recreate the props as `render` did originally or read the DOM ' +
86
+ 'properties/attributes directly from this node (e.g., ' +
87
+ 'this.refs.box.className).%s',
88
+ getDeclarationErrorAddendum(component)
89
+ );
90
+ return component._currentElement.props;
91
+ },
92
+ },
93
+ };
94
+}
95
+
96
+function legacyGetDOMNode() {
97
+ if (__DEV__) {
98
+ var component = this._reactInternalComponent;
99
+ warning(
100
+ false,
101
+ 'ReactDOMComponent: Do not access .getDOMNode() of a DOM node; ' +
102
+ 'instead, use the node directly.%s',
103
+ getDeclarationErrorAddendum(component)
104
+ );
105
+ }
106
+ return this;
107
+}
108
+
109
+function legacyIsMounted() {
110
+ var component = this._reactInternalComponent;
111
+ if (__DEV__) {
112
+ warning(
113
+ false,
114
+ 'ReactDOMComponent: Do not access .isMounted() of a DOM node.%s',
115
+ getDeclarationErrorAddendum(component)
116
+ );
117
+ }
118
+ return !!component;
119
+}
120
+
121
+function legacySetStateEtc() {
122
+ if (__DEV__) {
123
+ var component = this._reactInternalComponent;
124
+ warning(
125
+ false,
126
+ 'ReactDOMComponent: Do not access .setState(), .replaceState(), or ' +
127
+ '.forceUpdate() of a DOM node. This is a no-op.%s',
128
+ getDeclarationErrorAddendum(component)
129
+ );
130
+ }
131
+}
132
+
133
+function legacySetProps(partialProps, callback) {
134
+ var component = this._reactInternalComponent;
135
+ if (__DEV__) {
136
+ warning(
137
+ false,
138
+ 'ReactDOMComponent: Do not access .setProps() of a DOM node. ' +
139
+ 'Instead, call React.render again at the top level.%s',
140
+ getDeclarationErrorAddendum(component)
141
+ );
142
+ }
143
+ if (!component) {
144
+ return;
145
+ }
146
+ ReactUpdateQueue.enqueueSetPropsInternal(component, partialProps);
147
+ if (callback) {
148
+ ReactUpdateQueue.enqueueCallbackInternal(component, callback);
149
+ }
150
+}
151
+
152
+function legacyReplaceProps(partialProps, callback) {
153
+ var component = this._reactInternalComponent;
154
+ if (__DEV__) {
155
+ warning(
156
+ false,
157
+ 'ReactDOMComponent: Do not access .replaceProps() of a DOM node. ' +
158
+ 'Instead, call React.render again at the top level.%s',
159
+ getDeclarationErrorAddendum(component)
160
+ );
161
+ }
162
+ if (!component) {
163
+ return;
164
+ }
165
+ ReactUpdateQueue.enqueueReplacePropsInternal(component, partialProps);
166
+ if (callback) {
167
+ ReactUpdateQueue.enqueueCallbackInternal(component, callback);
168
+ }
169
+}
170
+
171
var styleMutationWarning = {};
172
173
function checkAndWarnForMutatedStyle(style1, style2, component) {
@@ -327,6 +444,8 @@ function ReactDOMComponent(tag) {
444
this._previousStyleCopy = null;
445
this._rootNodeID = null;
446
this._wrapperState = null;
447
+ this._topLevelWrapper = null;
448
+ this._nodeWithLegacyProperties = null;
449
}
450
451
ReactDOMComponent.displayName = 'ReactDOMComponent';
@@ -589,6 +708,10 @@ ReactDOMComponent.Mixin = {
708
processChildContext(context, this)
709
);
710
711
+ if (!canDefineProperty && this._nodeWithLegacyProperties) {
712
+ this._nodeWithLegacyProperties.props = nextProps;
713
+ }
714
+
715
if (this._tag === 'select') {
716
// <select> value update needs to occur after <option> children
717
// reconciliation
@@ -818,10 +941,41 @@ ReactDOMComponent.Mixin = {
941
ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
942
this._rootNodeID = null;
943
this._wrapperState = null;
944
+ if (this._nodeWithLegacyProperties) {
945
+ var node = this._nodeWithLegacyProperties;
946
+ node._reactInternalComponent = null;
947
+ this._nodeWithLegacyProperties = null;
948
+ }
949
},
950
951
getPublicInstance: function() {
824
- return ReactMount.getNode(this._rootNodeID);
952
+ if (!this._nodeWithLegacyProperties) {
953
+ var node = ReactMount.getNode(this._rootNodeID);
954
+
955
+ node._reactInternalComponent = this;
956
+ node.getDOMNode = legacyGetDOMNode;
957
+ node.isMounted = legacyIsMounted;
958
+ node.setState = legacySetStateEtc;
959
+ node.replaceState = legacySetStateEtc;
960
+ node.forceUpdate = legacySetStateEtc;
961
+ node.setProps = legacySetProps;
962
+ node.replaceProps = legacyReplaceProps;
963
+
964
+ if (__DEV__) {
965
+ if (canDefineProperty) {
966
+ Object.defineProperties(node, legacyPropsDescriptor);
967
+ } else {
968
+ // updateComponent will update this property on subsequent renders
969
+ node.props = this._currentElement.props;
970
+ }
971
+ } else {
972
+ // updateComponent will update this property on subsequent renders
973
+ node.props = this._currentElement.props;
974
+ }
975
+
976
+ this._nodeWithLegacyProperties = node;
977
+ }
978
+ return this._nodeWithLegacyProperties;
979
},
980
981
};
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
+56
-9
@@ -921,30 +921,77 @@ describe('ReactDOMComponent', function() {
921
922
it('warns when accessing properties on DOM components', function() {
923
spyOn(console, 'error');
924
+ var innerDiv;
925
var Animal = React.createClass({
926
render: function() {
927
return <div ref="div">iguana</div>;
928
},
929
componentDidMount: function() {
930
+ innerDiv = this.refs.div;
931
+
932
void this.refs.div.props;
930
- void this.refs.div.setProps;
933
+ this.refs.div.setState();
934
+ expect(this.refs.div.getDOMNode()).toBe(this.refs.div);
935
+ expect(this.refs.div.isMounted()).toBe(true);
936
},
937
});
933
- ReactTestUtils.renderIntoDocument(<Animal />);
938
+ var container = document.createElement('div');
939
+ React.render(<Animal />, container);
940
+ React.unmountComponentAtNode(container);
941
+ expect(innerDiv.isMounted()).toBe(false);
942
+
943
+ expect(console.error.calls.length).toBe(5);
944
+ expect(console.error.calls[0].args[0]).toBe(
945
+ 'Warning: ReactDOMComponent: Do not access .props of a DOM ' +
946
+ 'node; instead, recreate the props as `render` did originally or ' +
947
+ 'read the DOM properties/attributes directly from this node (e.g., ' +
948
+ 'this.refs.box.className). This DOM node was rendered by `Animal`.'
949
+ );
950
+ expect(console.error.calls[1].args[0]).toBe(
951
+ 'Warning: ReactDOMComponent: Do not access .setState(), ' +
952
+ '.replaceState(), or .forceUpdate() of a DOM node. This is a no-op. ' +
953
+ 'This DOM node was rendered by `Animal`.'
954
+ );
955
+ expect(console.error.calls[2].args[0]).toBe(
956
+ 'Warning: ReactDOMComponent: Do not access .getDOMNode() of a DOM ' +
957
+ 'node; instead, use the node directly. This DOM node was ' +
958
+ 'rendered by `Animal`.'
959
+ );
960
+ expect(console.error.calls[3].args[0]).toBe(
961
+ 'Warning: ReactDOMComponent: Do not access .isMounted() of a DOM ' +
962
+ 'node. This DOM node was rendered by `Animal`.'
963
+ );
964
+ expect(console.error.calls[4].args[0]).toContain('isMounted');
965
+ });
966
+
967
+ it('handles legacy setProps and replaceProps', function() {
968
+ spyOn(console, 'error');
969
+ var node = ReactTestUtils.renderIntoDocument(<div>rhinoceros</div>);
970
+
971
+ node.setProps({className: 'herbiverous'});
972
+ expect(node.className).toBe('herbiverous');
973
+ expect(node.textContent).toBe('rhinoceros');
974
+
975
+ node.replaceProps({className: 'invisible rhino'});
976
+ expect(node.className).toBe('invisible rhino');
977
+ expect(node.textContent).toBe('');
978
979
expect(console.error.calls.length).toBe(2);
980
expect(console.error.calls[0].args[0]).toBe(
937
- 'Warning: ReactDOMComponent.props: Do not access .props of a DOM ' +
938
- 'component directly; instead, recreate the props as `render` did ' +
939
- 'originally or use React.findDOMNode and read the DOM ' +
940
- 'properties/attributes directly. This DOM component was rendered ' +
941
- 'by `Animal`.'
981
+ 'Warning: ReactDOMComponent: Do not access .setProps() of a DOM node. ' +
982
+ 'Instead, call React.render again at the top level.'
983
);
984
expect(console.error.calls[1].args[0]).toBe(
944
- 'Warning: ReactDOMComponent.setProps(): Do not access .setProps() of ' +
945
- 'a DOM component. This DOM component was rendered by `Animal`.'
985
+ 'Warning: ReactDOMComponent: Do not access .replaceProps() of a DOM ' +
986
+ 'node. Instead, call React.render again at the top level.'
987
);
988
});
989
+
990
+ it('does not touch ref-less nodes', function() {
991
+ var node = ReactTestUtils.renderIntoDocument(<div><span /></div>);
992
+ expect(typeof node.getDOMNode).toBe('function');
993
+ expect(typeof node.firstChild.getDOMNode).toBe('undefined');
994
+ });
995
});
996
997
});
src/renderers/shared/reconciler/ReactCompositeComponent.js
+2
-1
@@ -99,7 +99,7 @@ var ReactCompositeComponentMixin = {
99
100
this._context = null;
101
this._mountOrder = 0;
102
- this._isTopLevel = false;
102
+ this._topLevelWrapper = null;
103
104
// See ReactUpdates and ReactUpdateQueue.
105
this._pendingCallbacks = null;
@@ -277,6 +277,7 @@ var ReactCompositeComponentMixin = {
277
// longer accessible.
278
this._context = null;
279
this._rootNodeID = null;
280
+ this._topLevelWrapper = null;
281
282
// Delete the reference from the instance to this internal representation
283
// which allow the internals to be properly cleaned up even if the user
src/renderers/shared/reconciler/ReactUpdateQueue.js
+24
-16
@@ -245,13 +245,16 @@ var ReactUpdateQueue = {
245
publicInstance,
246
'setProps'
247
);
248
-
248
if (!internalInstance) {
249
return;
250
}
251
+ ReactUpdateQueue.enqueueSetPropsInternal(internalInstance, partialProps);
252
+ },
253
254
+ enqueueSetPropsInternal: function(internalInstance, partialProps) {
255
+ var topLevelWrapper = internalInstance._topLevelWrapper;
256
invariant(
254
- internalInstance._isTopLevel,
257
+ topLevelWrapper,
258
'setProps(...): You called `setProps` on a ' +
259
'component with a parent. This is an anti-pattern since props will ' +
260
'get reactively updated when rendered. Instead, change the owner\'s ' +
@@ -261,15 +264,16 @@ var ReactUpdateQueue = {
264
265
// Merge with the pending element if it exists, otherwise with existing
266
// element props.
264
- var element = internalInstance._pendingElement ||
265
- internalInstance._currentElement;
267
+ var wrapElement = topLevelWrapper._pendingElement ||
268
+ topLevelWrapper._currentElement;
269
+ var element = wrapElement.props;
270
var props = assign({}, element.props, partialProps);
267
- internalInstance._pendingElement = ReactElement.cloneAndReplaceProps(
268
- element,
269
- props
271
+ topLevelWrapper._pendingElement = ReactElement.cloneAndReplaceProps(
272
+ wrapElement,
273
+ ReactElement.cloneAndReplaceProps(element, props)
274
);
275
272
- enqueueUpdate(internalInstance);
276
+ enqueueUpdate(topLevelWrapper);
277
},
278
279
/**
@@ -284,13 +288,16 @@ var ReactUpdateQueue = {
288
publicInstance,
289
'replaceProps'
290
);
287
-
291
if (!internalInstance) {
292
return;
293
}
294
+ ReactUpdateQueue.enqueueReplacePropsInternal(internalInstance, props);
295
+ },
296
297
+ enqueueReplacePropsInternal: function(internalInstance, props) {
298
+ var topLevelWrapper = internalInstance._topLevelWrapper;
299
invariant(
293
- internalInstance._isTopLevel,
300
+ topLevelWrapper,
301
'replaceProps(...): You called `replaceProps` on a ' +
302
'component with a parent. This is an anti-pattern since props will ' +
303
'get reactively updated when rendered. Instead, change the owner\'s ' +
@@ -300,14 +307,15 @@ var ReactUpdateQueue = {
307
308
// Merge with the pending element if it exists, otherwise with existing
309
// element props.
303
- var element = internalInstance._pendingElement ||
304
- internalInstance._currentElement;
305
- internalInstance._pendingElement = ReactElement.cloneAndReplaceProps(
306
- element,
307
- props
310
+ var wrapElement = topLevelWrapper._pendingElement ||
311
+ topLevelWrapper._currentElement;
312
+ var element = wrapElement.props;
313
+ topLevelWrapper._pendingElement = ReactElement.cloneAndReplaceProps(
314
+ wrapElement,
315
+ ReactElement.cloneAndReplaceProps(element, props)
316
);
317
310
- enqueueUpdate(internalInstance);
318
+ enqueueUpdate(topLevelWrapper);
319
},
320
321
enqueueElementInternal: function(internalInstance, newElement) {
src/renderers/shared/reconciler/__tests__/ReactComponent-test.js
+7
-2
@@ -267,14 +267,19 @@ describe('ReactComponent', function() {
267
it('warns when calling getDOMNode', function() {
268
spyOn(console, 'error');
269
270
+ var Potato = React.createClass({
271
+ render: function() {
272
+ return <div />;
273
+ },
274
+ });
275
var container = document.createElement('div');
271
- var instance = React.render(<div />, container);
276
+ var instance = React.render(<Potato />, container);
277
278
instance.getDOMNode();
279
280
expect(console.error.calls.length).toBe(1);
281
expect(console.error.calls[0].args[0]).toContain(
277
- 'DIV.getDOMNode(...) is deprecated. Please use ' +
282
+ 'Potato.getDOMNode(...) is deprecated. Please use ' +
283
'React.findDOMNode(instance) instead.'
284
);
285
});