Add legacy methods to DOM components for compatibility

This commit is contained in:
Ben Alpert
2015-06-17 20:29:45 -07:00
parent ffd527f593
commit eefda9377c
6 changed files with 245 additions and 34 deletions
+1 -5
View File
@@ -274,11 +274,7 @@ function mountComponentIntoNode(
var markup = ReactReconciler.mountComponent(
componentInstance, rootID, transaction, context
);
if (typeof componentInstance._renderedComponent._currentElement.type ===
'function') {
// hax
componentInstance._renderedComponent._isTopLevel = true;
}
componentInstance._renderedComponent._topLevelWrapper = componentInstance;
ReactMount._mountImageIntoNode(markup, container, shouldReuseMarkup);
}
+155 -1
View File
@@ -30,6 +30,7 @@ var ReactDOMTextarea = require('ReactDOMTextarea');
var ReactMount = require('ReactMount');
var ReactMultiChild = require('ReactMultiChild');
var ReactPerf = require('ReactPerf');
var ReactUpdateQueue = require('ReactUpdateQueue');
var assign = require('Object.assign');
var escapeTextContentForBrowser = require('escapeTextContentForBrowser');
@@ -51,6 +52,122 @@ var STYLE = keyOf({style: null});
var ELEMENT_NODE_TYPE = 1;
var canDefineProperty = false;
try {
Object.defineProperty({}, 'test', {get: function() {}});
canDefineProperty = true;
} catch (e) {
}
function getDeclarationErrorAddendum(internalInstance) {
if (internalInstance) {
var owner = internalInstance._currentElement._owner || null;
if (owner) {
var name = owner.getName();
if (name) {
return ' This DOM node was rendered by `' + name + '`.';
}
}
}
return '';
}
var legacyPropsDescriptor;
if (__DEV__) {
legacyPropsDescriptor = {
props: {
enumerable: false,
get: function() {
var component = this._reactInternalComponent;
warning(
false,
'ReactDOMComponent: Do not access .props of a DOM node; instead, ' +
'recreate the props as `render` did originally or read the DOM ' +
'properties/attributes directly from this node (e.g., ' +
'this.refs.box.className).%s',
getDeclarationErrorAddendum(component)
);
return component._currentElement.props;
},
},
};
}
function legacyGetDOMNode() {
if (__DEV__) {
var component = this._reactInternalComponent;
warning(
false,
'ReactDOMComponent: Do not access .getDOMNode() of a DOM node; ' +
'instead, use the node directly.%s',
getDeclarationErrorAddendum(component)
);
}
return this;
}
function legacyIsMounted() {
var component = this._reactInternalComponent;
if (__DEV__) {
warning(
false,
'ReactDOMComponent: Do not access .isMounted() of a DOM node.%s',
getDeclarationErrorAddendum(component)
);
}
return !!component;
}
function legacySetStateEtc() {
if (__DEV__) {
var component = this._reactInternalComponent;
warning(
false,
'ReactDOMComponent: Do not access .setState(), .replaceState(), or ' +
'.forceUpdate() of a DOM node. This is a no-op.%s',
getDeclarationErrorAddendum(component)
);
}
}
function legacySetProps(partialProps, callback) {
var component = this._reactInternalComponent;
if (__DEV__) {
warning(
false,
'ReactDOMComponent: Do not access .setProps() of a DOM node. ' +
'Instead, call React.render again at the top level.%s',
getDeclarationErrorAddendum(component)
);
}
if (!component) {
return;
}
ReactUpdateQueue.enqueueSetPropsInternal(component, partialProps);
if (callback) {
ReactUpdateQueue.enqueueCallbackInternal(component, callback);
}
}
function legacyReplaceProps(partialProps, callback) {
var component = this._reactInternalComponent;
if (__DEV__) {
warning(
false,
'ReactDOMComponent: Do not access .replaceProps() of a DOM node. ' +
'Instead, call React.render again at the top level.%s',
getDeclarationErrorAddendum(component)
);
}
if (!component) {
return;
}
ReactUpdateQueue.enqueueReplacePropsInternal(component, partialProps);
if (callback) {
ReactUpdateQueue.enqueueCallbackInternal(component, callback);
}
}
var styleMutationWarning = {};
function checkAndWarnForMutatedStyle(style1, style2, component) {
@@ -327,6 +444,8 @@ function ReactDOMComponent(tag) {
this._previousStyleCopy = null;
this._rootNodeID = null;
this._wrapperState = null;
this._topLevelWrapper = null;
this._nodeWithLegacyProperties = null;
}
ReactDOMComponent.displayName = 'ReactDOMComponent';
@@ -589,6 +708,10 @@ ReactDOMComponent.Mixin = {
processChildContext(context, this)
);
if (!canDefineProperty && this._nodeWithLegacyProperties) {
this._nodeWithLegacyProperties.props = nextProps;
}
if (this._tag === 'select') {
// <select> value update needs to occur after <option> children
// reconciliation
@@ -818,10 +941,41 @@ ReactDOMComponent.Mixin = {
ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
this._rootNodeID = null;
this._wrapperState = null;
if (this._nodeWithLegacyProperties) {
var node = this._nodeWithLegacyProperties;
node._reactInternalComponent = null;
this._nodeWithLegacyProperties = null;
}
},
getPublicInstance: function() {
return ReactMount.getNode(this._rootNodeID);
if (!this._nodeWithLegacyProperties) {
var node = ReactMount.getNode(this._rootNodeID);
node._reactInternalComponent = this;
node.getDOMNode = legacyGetDOMNode;
node.isMounted = legacyIsMounted;
node.setState = legacySetStateEtc;
node.replaceState = legacySetStateEtc;
node.forceUpdate = legacySetStateEtc;
node.setProps = legacySetProps;
node.replaceProps = legacyReplaceProps;
if (__DEV__) {
if (canDefineProperty) {
Object.defineProperties(node, legacyPropsDescriptor);
} else {
// updateComponent will update this property on subsequent renders
node.props = this._currentElement.props;
}
} else {
// updateComponent will update this property on subsequent renders
node.props = this._currentElement.props;
}
this._nodeWithLegacyProperties = node;
}
return this._nodeWithLegacyProperties;
},
};
@@ -921,30 +921,77 @@ describe('ReactDOMComponent', function() {
it('warns when accessing properties on DOM components', function() {
spyOn(console, 'error');
var innerDiv;
var Animal = React.createClass({
render: function() {
return <div ref="div">iguana</div>;
},
componentDidMount: function() {
innerDiv = this.refs.div;
void this.refs.div.props;
void this.refs.div.setProps;
this.refs.div.setState();
expect(this.refs.div.getDOMNode()).toBe(this.refs.div);
expect(this.refs.div.isMounted()).toBe(true);
},
});
ReactTestUtils.renderIntoDocument(<Animal />);
var container = document.createElement('div');
React.render(<Animal />, container);
React.unmountComponentAtNode(container);
expect(innerDiv.isMounted()).toBe(false);
expect(console.error.calls.length).toBe(5);
expect(console.error.calls[0].args[0]).toBe(
'Warning: ReactDOMComponent: Do not access .props of a DOM ' +
'node; instead, recreate the props as `render` did originally or ' +
'read the DOM properties/attributes directly from this node (e.g., ' +
'this.refs.box.className). This DOM node was rendered by `Animal`.'
);
expect(console.error.calls[1].args[0]).toBe(
'Warning: ReactDOMComponent: Do not access .setState(), ' +
'.replaceState(), or .forceUpdate() of a DOM node. This is a no-op. ' +
'This DOM node was rendered by `Animal`.'
);
expect(console.error.calls[2].args[0]).toBe(
'Warning: ReactDOMComponent: Do not access .getDOMNode() of a DOM ' +
'node; instead, use the node directly. This DOM node was ' +
'rendered by `Animal`.'
);
expect(console.error.calls[3].args[0]).toBe(
'Warning: ReactDOMComponent: Do not access .isMounted() of a DOM ' +
'node. This DOM node was rendered by `Animal`.'
);
expect(console.error.calls[4].args[0]).toContain('isMounted');
});
it('handles legacy setProps and replaceProps', function() {
spyOn(console, 'error');
var node = ReactTestUtils.renderIntoDocument(<div>rhinoceros</div>);
node.setProps({className: 'herbiverous'});
expect(node.className).toBe('herbiverous');
expect(node.textContent).toBe('rhinoceros');
node.replaceProps({className: 'invisible rhino'});
expect(node.className).toBe('invisible rhino');
expect(node.textContent).toBe('');
expect(console.error.calls.length).toBe(2);
expect(console.error.calls[0].args[0]).toBe(
'Warning: ReactDOMComponent.props: Do not access .props of a DOM ' +
'component directly; instead, recreate the props as `render` did ' +
'originally or use React.findDOMNode and read the DOM ' +
'properties/attributes directly. This DOM component was rendered ' +
'by `Animal`.'
'Warning: ReactDOMComponent: Do not access .setProps() of a DOM node. ' +
'Instead, call React.render again at the top level.'
);
expect(console.error.calls[1].args[0]).toBe(
'Warning: ReactDOMComponent.setProps(): Do not access .setProps() of ' +
'a DOM component. This DOM component was rendered by `Animal`.'
'Warning: ReactDOMComponent: Do not access .replaceProps() of a DOM ' +
'node. Instead, call React.render again at the top level.'
);
});
it('does not touch ref-less nodes', function() {
var node = ReactTestUtils.renderIntoDocument(<div><span /></div>);
expect(typeof node.getDOMNode).toBe('function');
expect(typeof node.firstChild.getDOMNode).toBe('undefined');
});
});
});
@@ -99,7 +99,7 @@ var ReactCompositeComponentMixin = {
this._context = null;
this._mountOrder = 0;
this._isTopLevel = false;
this._topLevelWrapper = null;
// See ReactUpdates and ReactUpdateQueue.
this._pendingCallbacks = null;
@@ -277,6 +277,7 @@ var ReactCompositeComponentMixin = {
// longer accessible.
this._context = null;
this._rootNodeID = null;
this._topLevelWrapper = null;
// Delete the reference from the instance to this internal representation
// which allow the internals to be properly cleaned up even if the user
@@ -245,13 +245,16 @@ var ReactUpdateQueue = {
publicInstance,
'setProps'
);
if (!internalInstance) {
return;
}
ReactUpdateQueue.enqueueSetPropsInternal(internalInstance, partialProps);
},
enqueueSetPropsInternal: function(internalInstance, partialProps) {
var topLevelWrapper = internalInstance._topLevelWrapper;
invariant(
internalInstance._isTopLevel,
topLevelWrapper,
'setProps(...): You called `setProps` on a ' +
'component with a parent. This is an anti-pattern since props will ' +
'get reactively updated when rendered. Instead, change the owner\'s ' +
@@ -261,15 +264,16 @@ var ReactUpdateQueue = {
// Merge with the pending element if it exists, otherwise with existing
// element props.
var element = internalInstance._pendingElement ||
internalInstance._currentElement;
var wrapElement = topLevelWrapper._pendingElement ||
topLevelWrapper._currentElement;
var element = wrapElement.props;
var props = assign({}, element.props, partialProps);
internalInstance._pendingElement = ReactElement.cloneAndReplaceProps(
element,
props
topLevelWrapper._pendingElement = ReactElement.cloneAndReplaceProps(
wrapElement,
ReactElement.cloneAndReplaceProps(element, props)
);
enqueueUpdate(internalInstance);
enqueueUpdate(topLevelWrapper);
},
/**
@@ -284,13 +288,16 @@ var ReactUpdateQueue = {
publicInstance,
'replaceProps'
);
if (!internalInstance) {
return;
}
ReactUpdateQueue.enqueueReplacePropsInternal(internalInstance, props);
},
enqueueReplacePropsInternal: function(internalInstance, props) {
var topLevelWrapper = internalInstance._topLevelWrapper;
invariant(
internalInstance._isTopLevel,
topLevelWrapper,
'replaceProps(...): You called `replaceProps` on a ' +
'component with a parent. This is an anti-pattern since props will ' +
'get reactively updated when rendered. Instead, change the owner\'s ' +
@@ -300,14 +307,15 @@ var ReactUpdateQueue = {
// Merge with the pending element if it exists, otherwise with existing
// element props.
var element = internalInstance._pendingElement ||
internalInstance._currentElement;
internalInstance._pendingElement = ReactElement.cloneAndReplaceProps(
element,
props
var wrapElement = topLevelWrapper._pendingElement ||
topLevelWrapper._currentElement;
var element = wrapElement.props;
topLevelWrapper._pendingElement = ReactElement.cloneAndReplaceProps(
wrapElement,
ReactElement.cloneAndReplaceProps(element, props)
);
enqueueUpdate(internalInstance);
enqueueUpdate(topLevelWrapper);
},
enqueueElementInternal: function(internalInstance, newElement) {
@@ -267,14 +267,19 @@ describe('ReactComponent', function() {
it('warns when calling getDOMNode', function() {
spyOn(console, 'error');
var Potato = React.createClass({
render: function() {
return <div />;
},
});
var container = document.createElement('div');
var instance = React.render(<div />, container);
var instance = React.render(<Potato />, container);
instance.getDOMNode();
expect(console.error.calls.length).toBe(1);
expect(console.error.calls[0].args[0]).toContain(
'DIV.getDOMNode(...) is deprecated. Please use ' +
'Potato.getDOMNode(...) is deprecated. Please use ' +
'React.findDOMNode(instance) instead.'
);
});