Skip to content

Commit 0d190f1

Browse files
committed
Show component stack in PropTypes warnings
1 parent 3cc733a commit 0d190f1

File tree

13 files changed

+244
-42
lines changed

13 files changed

+244
-42
lines changed

‎src/isomorphic/ReactDebugTool.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ var ReactDebugTool = {
236236
checkDebugID(debugID);
237237
emitEvent('onSetOwner', debugID, ownerDebugID);
238238
},
239+
onSetParent(debugID, parentDebugID) {
240+
checkDebugID(debugID);
241+
emitEvent('onSetParent', debugID, parentDebugID);
242+
},
239243
onSetText(debugID, text) {
240244
checkDebugID(debugID);
241245
emitEvent('onSetText', debugID, text);

‎src/isomorphic/classic/element/ReactElementValidator.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818

1919
'use strict';
2020

21+
var ReactCurrentOwner = require('ReactCurrentOwner');
22+
var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
2123
var ReactElement = require('ReactElement');
22-
var ReactPropTypeLocations = require('ReactPropTypeLocations');
2324
var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
24-
var ReactCurrentOwner = require('ReactCurrentOwner');
25+
var ReactPropTypeLocations = require('ReactPropTypeLocations');
2526

2627
var canDefineProperty = require('canDefineProperty');
2728
var getIteratorFn = require('getIteratorFn');
@@ -171,13 +172,14 @@ function validateChildKeys(node, parentType) {
171172
/**
172173
* Assert that the props are valid
173174
*
175+
* @param {object} element
174176
* @param {string} componentName Name of the component for error messages.
175177
* @param {object} propTypes Map of prop name to a ReactPropType
176-
* @param {object} props
177178
* @param {string} location e.g. "prop", "context", "child context"
178179
* @private
179180
*/
180-
function checkPropTypes(componentName, propTypes, props, location) {
181+
function checkPropTypes(element, componentName, propTypes, location) {
182+
var props = element.props;
181183
for (var propName in propTypes) {
182184
if (propTypes.hasOwnProperty(propName)) {
183185
var error;
@@ -216,8 +218,12 @@ function checkPropTypes(componentName, propTypes, props, location) {
216218
// same error.
217219
loggedTypeFailures[error.message] = true;
218220

219-
var addendum = getDeclarationErrorAddendum();
220-
warning(false, 'Failed propType: %s%s', error.message, addendum);
221+
warning(
222+
false,
223+
'Failed propType: %s%s',
224+
error.message,
225+
ReactComponentTreeDevtool.getCurrentStackAddendum(element)
226+
);
221227
}
222228
}
223229
}
@@ -237,9 +243,9 @@ function validatePropTypes(element) {
237243
var name = componentClass.displayName || componentClass.name;
238244
if (componentClass.propTypes) {
239245
checkPropTypes(
246+
element,
240247
name,
241248
componentClass.propTypes,
242-
element.props,
243249
ReactPropTypeLocations.prop
244250
);
245251
}

‎src/isomorphic/classic/element/__tests__/ReactElementClone-test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,10 @@ describe('ReactElementClone', function() {
271271
expect(console.error.argsForCall[0][0]).toBe(
272272
'Warning: Failed propType: ' +
273273
'Invalid prop `color` of type `number` supplied to `Component`, ' +
274-
'expected `string`. Check the render method of `Parent`.'
274+
'expected `string`.\n' +
275+
' in Component (created by GrandParent)\n' +
276+
' in Parent (created by GrandParent)\n' +
277+
' in GrandParent'
275278
);
276279
});
277280

‎src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,9 @@ describe('ReactElementValidator', function() {
242242
expect(console.error.argsForCall[0][0]).toBe(
243243
'Warning: Failed propType: ' +
244244
'Invalid prop `color` of type `number` supplied to `MyComp`, ' +
245-
'expected `string`. Check the render method of `ParentComp`.'
245+
'expected `string`.\n' +
246+
' in MyComp (created by ParentComp)\n' +
247+
' in ParentComp'
246248
);
247249
});
248250

@@ -318,7 +320,8 @@ describe('ReactElementValidator', function() {
318320
expect(console.error.calls.length).toBe(1);
319321
expect(console.error.argsForCall[0][0]).toBe(
320322
'Warning: Failed propType: ' +
321-
'Required prop `prop` was not specified in `Component`.'
323+
'Required prop `prop` was not specified in `Component`.\n' +
324+
' in Component'
322325
);
323326
});
324327

@@ -342,7 +345,8 @@ describe('ReactElementValidator', function() {
342345
expect(console.error.calls.length).toBe(1);
343346
expect(console.error.argsForCall[0][0]).toBe(
344347
'Warning: Failed propType: ' +
345-
'Required prop `prop` was not specified in `Component`.'
348+
'Required prop `prop` was not specified in `Component`.\n' +
349+
' in Component'
346350
);
347351
});
348352

@@ -368,13 +372,15 @@ describe('ReactElementValidator', function() {
368372
expect(console.error.calls.length).toBe(2);
369373
expect(console.error.argsForCall[0][0]).toBe(
370374
'Warning: Failed propType: ' +
371-
'Required prop `prop` was not specified in `Component`.'
375+
'Required prop `prop` was not specified in `Component`.\n' +
376+
' in Component'
372377
);
373378

374379
expect(console.error.argsForCall[1][0]).toBe(
375380
'Warning: Failed propType: ' +
376381
'Invalid prop `prop` of type `number` supplied to ' +
377-
'`Component`, expected `string`.'
382+
'`Component`, expected `string`.\n' +
383+
' in Component'
378384
);
379385

380386
ReactTestUtils.renderIntoDocument(

‎src/isomorphic/classic/types/__tests__/ReactPropTypes-test.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -891,8 +891,11 @@ describe('ReactPropTypes', function() {
891891
var instance = <Component num={6} />;
892892
instance = ReactTestUtils.renderIntoDocument(instance);
893893
expect(console.error.argsForCall.length).toBe(1);
894-
expect(console.error.argsForCall[0][0]).toBe(
895-
'Warning: Failed propType: num must be 5!'
894+
expect(
895+
console.error.argsForCall[0][0].replace(/\(at .+?:\d+\)/g, '(at **)')
896+
).toBe(
897+
'Warning: Failed propType: num must be 5!\n' +
898+
' in Component (at **)'
896899
);
897900
});
898901

‎src/isomorphic/devtools/ReactComponentTreeDevtool.js

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
'use strict';
1313

14+
var ReactCurrentOwner = require('ReactCurrentOwner');
15+
1416
var invariant = require('invariant');
1517

1618
var tree = {};
@@ -53,7 +55,6 @@ var ReactComponentTreeDevtool = {
5355

5456
onSetChildren(id, nextChildIDs) {
5557
updateTree(id, item => {
56-
var prevChildIDs = item.childIDs;
5758
item.childIDs = nextChildIDs;
5859

5960
nextChildIDs.forEach(nextChildID => {
@@ -78,10 +79,11 @@ var ReactComponentTreeDevtool = {
7879
'Expected onMountComponent() to fire for the child ' +
7980
'before its parent includes it in onSetChildren().'
8081
);
81-
82-
if (prevChildIDs.indexOf(nextChildID) === -1) {
83-
nextChild.parentID = id;
84-
}
82+
nextChild.parentID = id;
83+
// TODO: invariant(nextChild.parentID === id) makes sense but doesn't
84+
// quite work because mounting a new root in componentWillMount
85+
// currently causes not-yet-mounted components to be erased from our
86+
// data and their parent ID is missing.
8587
});
8688
});
8789
},
@@ -90,6 +92,10 @@ var ReactComponentTreeDevtool = {
9092
updateTree(id, item => item.ownerID = ownerID);
9193
},
9294

95+
onSetParent(id, parentID) {
96+
updateTree(id, item => item.parentID = parentID);
97+
},
98+
9399
onSetText(id, text) {
94100
updateTree(id, item => item.text = text);
95101
},
@@ -138,6 +144,53 @@ var ReactComponentTreeDevtool = {
138144
return item ? item.isMounted : false;
139145
},
140146

147+
getCurrentStackAddendum(topElement) {
148+
function describeComponentFrame(name, source, ownerName) {
149+
return '\n in ' + name + (
150+
source ?
151+
' (at ' + source.fileName.replace(/^.*[\\\/]/, '') + ':' +
152+
source.lineNumber + ')' :
153+
ownerName ?
154+
' (created by ' + ownerName + ')' :
155+
''
156+
);
157+
}
158+
159+
function describeID(id) {
160+
var name = ReactComponentTreeDevtool.getDisplayName(id);
161+
var element = ReactComponentTreeDevtool.getElement(id);
162+
var ownerID = ReactComponentTreeDevtool.getOwnerID(id);
163+
var ownerName;
164+
if (ownerID) {
165+
ownerName = ReactComponentTreeDevtool.getDisplayName(ownerID);
166+
}
167+
return describeComponentFrame(name, element._source, ownerName);
168+
}
169+
170+
var info = '';
171+
if (topElement) {
172+
var type = topElement.type;
173+
var name = typeof type === 'function' ?
174+
type.displayName || type.name :
175+
type;
176+
var owner = topElement._owner;
177+
info += describeComponentFrame(
178+
name || 'Unknown',
179+
topElement._source,
180+
owner && owner.getName()
181+
);
182+
}
183+
184+
var currentOwner = ReactCurrentOwner.current;
185+
var id = currentOwner && currentOwner._debugID;
186+
while (id) {
187+
info += describeID(id);
188+
id = ReactComponentTreeDevtool.getParentID(id);
189+
}
190+
191+
return info;
192+
},
193+
141194
getChildIDs(id) {
142195
var item = tree[id];
143196
return item ? item.childIDs : [];
@@ -148,6 +201,11 @@ var ReactComponentTreeDevtool = {
148201
return item ? item.displayName : 'Unknown';
149202
},
150203

204+
getElement(id) {
205+
var item = tree[id];
206+
return item ? item.element : null;
207+
},
208+
151209
getOwnerID(id) {
152210
var item = tree[id];
153211
return item ? item.ownerID : null;

‎src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,4 +1741,78 @@ describe('ReactComponentTreeDevtool', () => {
17411741
expect(getRootDisplayNames()).toEqual([]);
17421742
expect(getRegisteredDisplayNames()).toEqual([]);
17431743
});
1744+
1745+
it('creates stack addenda', () => {
1746+
function getAddendum(element) {
1747+
var addendum = ReactComponentTreeDevtool.getCurrentStackAddendum(element);
1748+
return addendum.replace(/\(at .+?:\d+\)/g, '(at **)');
1749+
}
1750+
1751+
var Anon = React.createClass({displayName: null, render: () => null});
1752+
var Orange = React.createClass({render: () => null});
1753+
1754+
expect(getAddendum()).toBe(
1755+
''
1756+
);
1757+
expect(getAddendum(<div />)).toBe(
1758+
'\n in div (at **)'
1759+
);
1760+
expect(getAddendum(<Anon />)).toBe(
1761+
'\n in Unknown (at **)'
1762+
);
1763+
expect(getAddendum(<Orange />)).toBe(
1764+
'\n in Orange (at **)'
1765+
);
1766+
expect(getAddendum(React.createElement(Orange))).toBe(
1767+
'\n in Orange'
1768+
);
1769+
1770+
var renders = 0;
1771+
var rOwnedByQ;
1772+
1773+
function Q() {
1774+
return (rOwnedByQ = React.createElement(R));
1775+
}
1776+
function R() {
1777+
return <div><S /></div>;
1778+
}
1779+
class S extends React.Component {
1780+
componentDidMount() {
1781+
// Check that the parent path is still fetched when only S itself is on
1782+
// the stack.
1783+
this.forceUpdate();
1784+
}
1785+
render() {
1786+
expect(getAddendum()).toBe(
1787+
'\n in S (at **)' +
1788+
'\n in div (at **)' +
1789+
'\n in R (created by Q)' +
1790+
'\n in Q (at **)'
1791+
);
1792+
expect(getAddendum(<span />)).toBe(
1793+
'\n in span (at **)' +
1794+
'\n in S (at **)' +
1795+
'\n in div (at **)' +
1796+
'\n in R (created by Q)' +
1797+
'\n in Q (at **)'
1798+
);
1799+
expect(getAddendum(React.createElement('span'))).toBe(
1800+
'\n in span (created by S)' +
1801+
'\n in S (at **)' +
1802+
'\n in div (at **)' +
1803+
'\n in R (created by Q)' +
1804+
'\n in Q (at **)'
1805+
);
1806+
renders++;
1807+
return null;
1808+
}
1809+
}
1810+
ReactDOM.render(<Q />, document.createElement('div'));
1811+
expect(renders).toBe(2);
1812+
1813+
// Make sure owner is fetched for the top element too.
1814+
expect(getAddendum(rOwnedByQ)).toBe(
1815+
'\n in R (created by Q)'
1816+
);
1817+
});
17441818
});

0 commit comments

Comments
 (0)