@samitouri / QOS-React-2 / commits / eefda9377c

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