Skip to content

Commit 397a15a

Browse files
committed
Show component stack in PropTypes warnings
1 parent 3cc733a commit 397a15a

File tree

10 files changed

+235
-27
lines changed

10 files changed

+235
-27
lines changed

src/isomorphic/classic/element/ReactElementValidator.js

+13-7
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

+4-1
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

+11-5
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

+5-2
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

+92
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,33 @@
1212
'use strict';
1313

1414
var invariant = require('invariant');
15+
var warning = require('warning');
1516

1617
var tree = {};
1718
var unmountedIDs = {};
1819
var rootIDs = {};
1920

21+
var processingStack = [];
22+
23+
function startProcessing(id) {
24+
processingStack.push(id);
25+
}
26+
27+
function stopProcessing(id) {
28+
// This should just be a single .pop() in most cases, but maybe not if there
29+
// was an exception or similar.
30+
do {
31+
if (!processingStack.length) {
32+
warning(
33+
false,
34+
'ReactComponentTreeDevtool: stopProcessing(%s) called, no match found.',
35+
id
36+
);
37+
return;
38+
}
39+
} while (processingStack.pop() !== id);
40+
}
41+
2042
function updateTree(id, update) {
2143
if (!tree[id]) {
2244
tree[id] = {
@@ -96,15 +118,18 @@ var ReactComponentTreeDevtool = {
96118

97119
onBeforeMountComponent(id, element) {
98120
updateTree(id, item => item.element = element);
121+
startProcessing(id);
99122
},
100123

101124
onBeforeUpdateComponent(id, element) {
102125
updateTree(id, item => item.element = element);
126+
startProcessing(id);
103127
},
104128

105129
onMountComponent(id) {
106130
updateTree(id, item => item.isMounted = true);
107131
delete unmountedIDs[id];
132+
stopProcessing(id);
108133
},
109134

110135
onMountRootComponent(id) {
@@ -113,6 +138,7 @@ var ReactComponentTreeDevtool = {
113138

114139
onUpdateComponent(id) {
115140
updateTree(id, item => item.updateCount++);
141+
stopProcessing(id);
116142
},
117143

118144
onUnmountComponent(id) {
@@ -138,6 +164,67 @@ var ReactComponentTreeDevtool = {
138164
return item ? item.isMounted : false;
139165
},
140166

167+
getCurrentStackAddendum(topElement) {
168+
function describeComponentFrame(name, source, ownerName) {
169+
return '\n in ' + name + (
170+
source ?
171+
' (at ' + source.fileName.replace(/^.*[\\\/]/, '') + ':' +
172+
source.lineNumber + ')' :
173+
ownerName ?
174+
' (created by ' + ownerName + ')' :
175+
''
176+
);
177+
}
178+
179+
function describeID(id) {
180+
var name = ReactComponentTreeDevtool.getDisplayName(id);
181+
var element = ReactComponentTreeDevtool.getElement(id);
182+
if (!element) {
183+
// TODO: This check shouldn't be necessary, but in the case that a mount
184+
// gets aborted due to an exception, processingStack has leftover
185+
// frames. In contrast, if we clear frames in purgeUnmountedComponents
186+
// then we get confused if someone starts a new root during a render.
187+
// Both of these are silly (and cause visible warnings or errors) but
188+
// are regrettably barely supported.
189+
return '';
190+
}
191+
var ownerID = ReactComponentTreeDevtool.getOwnerID(id);
192+
var ownerName;
193+
if (ownerID) {
194+
ownerName = ReactComponentTreeDevtool.getDisplayName(ownerID);
195+
}
196+
return describeComponentFrame(name, element._source, ownerName);
197+
}
198+
199+
var info = '';
200+
if (topElement) {
201+
var type = topElement.type;
202+
var name = typeof type === 'function' ?
203+
type.displayName || type.name :
204+
type;
205+
var owner = topElement._owner;
206+
info += describeComponentFrame(
207+
name || 'Unknown',
208+
topElement._source,
209+
owner && owner.getName()
210+
);
211+
}
212+
213+
var ii = processingStack.length;
214+
if (ii) {
215+
var id;
216+
while (ii-- > 0) {
217+
id = processingStack[ii];
218+
info += describeID(id);
219+
}
220+
while ((id = ReactComponentTreeDevtool.getParentID(id))) {
221+
info += describeID(id);
222+
}
223+
}
224+
225+
return info;
226+
},
227+
141228
getChildIDs(id) {
142229
var item = tree[id];
143230
return item ? item.childIDs : [];
@@ -148,6 +235,11 @@ var ReactComponentTreeDevtool = {
148235
return item ? item.displayName : 'Unknown';
149236
},
150237

238+
getElement(id) {
239+
var item = tree[id];
240+
return item ? item.element : null;
241+
},
242+
151243
getOwnerID(id) {
152244
var item = tree[id];
153245
return item ? item.ownerID : null;

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

+74
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)