diff --git a/src/browser/ui/ReactMount.js b/src/browser/ui/ReactMount.js index 581a904427da4..a18fde9cf4634 100644 --- a/src/browser/ui/ReactMount.js +++ b/src/browser/ui/ReactMount.js @@ -79,11 +79,15 @@ function getID(node) { if (nodeCache.hasOwnProperty(id)) { var cached = nodeCache[id]; if (cached !== node) { - invariant( - !isValid(cached, id), - 'ReactMount: Two valid but unequal nodes with the same `%s`: %s', - ATTR_NAME, id - ); + if (isValid(cached, id)) { + invariant( + false, + 'ReactMount: Two valid but unequal nodes with the same `%s`: %s. ' + + '%s', + ATTR_NAME, id, + getFriendlyLocationForID(id) + ); + } nodeCache[id] = node; } @@ -143,11 +147,14 @@ function getNode(id) { */ function isValid(node, id) { if (node) { - invariant( - internalGetID(node) === id, - 'ReactMount: Unexpected modification of `%s`', - ATTR_NAME - ); + if (internalGetID(node) !== id) { + invariant( + false, + 'ReactMount: Unexpected modification of `%s`. %s', + ATTR_NAME, + getFriendlyLocationForID(id) + ); + } var container = ReactMount.findReactContainerForID(id); if (container && containsNode(container, node)) { @@ -194,6 +201,118 @@ function findDeepestCachedAncestor(targetID) { return foundNode; } +/** + * For DEV-use only! + * Finds all descendant instances between the current instance and the tagetID, + * also returns the provided instance. + */ +function findDescendantInstancesByID(instance, targetID) { + var result = false; + var nodeID = instance._rootNodeID; + + if (nodeID === targetID) { + // `_rootNodeID` is owned by the ReactDOMComponent, but all + // ReactCompositeComponents before it are given the same ID, we want all. + if (instance._renderedComponent) { + result = findDescendantInstancesByID( + instance._renderedComponent, + targetID + ); + } + + result = result || []; + } else if (ReactInstanceHandles.isAncestorIDOf(nodeID, targetID)) { + if (instance._renderedComponent) { + // `instance` is a ReactCompositeComponent + result = findDescendantInstancesByID( + instance._renderedComponent, + targetID + ); + } else if (instance._renderedChildren) { + // `instance` is a ReactDOMComponent (ReactMultiChild) + var children = instance._renderedChildren; + + for (var key in children) { + result = findDescendantInstancesByID( + children[key], + targetID + ); + + // `targetID` found inside the current instance + if (result) { + break; + } + } + } + } + + if (result) { + // `targetID` found, add instance to the front of the result. + result.unshift(instance); + } + + return result; +} + +/** + * For DEV-use only! + * Constructs a human friendly path from an array of parent instances. + */ +function getReadableInstancePath(instances) { + var path = []; + var instance; + var parentNodeID = ''; + + for (var i = 0; instance = instances[i]; i++) { + var displayName; + + if (instance.tagName) { + // `instance` is a ReactDOMComponent + // Include the relative reactID with each `tagName` + displayName = ( + instance.tagName.toUpperCase() + + instance._rootNodeID.substr(parentNodeID.length) + ); + + parentNodeID = instance._rootNodeID; + } else { + // `instance` is a React(Composite)Component + displayName = instance.constructor.displayName || 'Composite'; + + // Do not include internally overloaded ReactDOMComponents, they are an + // implementation detail and would only confuse the user. + if (displayName.substr(0, 8) === 'ReactDOM') { + continue; + } + } + + path.push(displayName); + } + + return path.join(' > '); +} + +/** + * For DEV-use only! + * Returns a human friendly string for easily locating a target ID. For use in + * error messages involving a reactID. + */ +function getFriendlyLocationForID(targetID) { + var rootID = ReactInstanceHandles.getReactRootIDFromNodeID(targetID); + var rootInstance = instancesByReactRootID[rootID]; + var instances = findDescendantInstancesByID(rootInstance, targetID); + + if (!instances) { + // This should never occur unless React's internal state is corrupted. + return 'Unable to find target instance'; + } + + return ( + 'Inspect: ' + + getReadableInstancePath(instances) + ); +} + /** * Mounting is the process of initializing a React component by creatings its * representative DOM elements and inserting them into a supplied `container`. @@ -651,10 +770,9 @@ var ReactMount = { 'means the DOM was unexpectedly mutated (e.g., by the browser), ' + 'usually due to forgetting a when using tables, nesting tags ' + 'like
,

, or , or using non-SVG elements in an ' + - 'parent. ' + - 'Try inspecting the child nodes of the element with React ID `%s`.', + 'parent. %s', targetID, - ReactMount.getID(ancestorNode) + getFriendlyLocationForID(targetID) ); }, diff --git a/src/core/__tests__/ReactInstanceHandles-test.js b/src/core/__tests__/ReactInstanceHandles-test.js index bd8a4ff6debc1..1465b8435e1ee 100644 --- a/src/core/__tests__/ReactInstanceHandles-test.js +++ b/src/core/__tests__/ReactInstanceHandles-test.js @@ -144,13 +144,11 @@ describe('ReactInstanceHandles', function() { ReactMount.getID(childNodeB) + ":junk" ); }).toThrow( - 'Invariant Violation: findComponentRoot(..., .0.1:0:junk): ' + - 'Unable to find element. This probably means the DOM was ' + - 'unexpectedly mutated (e.g., by the browser), usually due to ' + - 'forgetting a when using tables, nesting tags ' + - 'like
,

, or , or using non-SVG elements in an ' + - 'parent. ' + - 'Try inspecting the child nodes of the element with React ID `.0`.' + 'Invariant Violation: findComponentRoot(..., .0.1:0:junk): Unable to ' + + 'find element. This probably means the DOM was unexpectedly mutated ' + + '(e.g., by the browser), usually due to forgetting a when ' + + 'using tables, nesting tags like
,

, or , or using ' + + 'non-SVG elements in an parent. Unable to find target instance' ); }); });