Show component stack in PropTypes warnings (#6771)
Ben Alpert committed
May 14, 2016 at 17:40 UTC
378c879a6a65afe18acacb8e2a6fe6ea323974df
13 files changed
+251
-40
src/isomorphic/ReactDebugTool.js
+4
@@ -236,6 +236,10 @@ var ReactDebugTool = {
236
checkDebugID(debugID);
237
emitEvent('onSetOwner', debugID, ownerDebugID);
238
},
239
+ onSetParent(debugID, parentDebugID) {
240
+ checkDebugID(debugID);
241
+ emitEvent('onSetParent', debugID, parentDebugID);
242
+ },
243
onSetText(debugID, text) {
244
checkDebugID(debugID);
245
emitEvent('onSetText', debugID, text);
src/isomorphic/classic/element/ReactElementValidator.js
+13
-7
@@ -18,10 +18,11 @@
18
19
'use strict';
20
21
+var ReactCurrentOwner = require('ReactCurrentOwner');
22
+var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
23
var ReactElement = require('ReactElement');
22
-var ReactPropTypeLocations = require('ReactPropTypeLocations');
24
var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
24
-var ReactCurrentOwner = require('ReactCurrentOwner');
25
+var ReactPropTypeLocations = require('ReactPropTypeLocations');
26
27
var canDefineProperty = require('canDefineProperty');
28
var getIteratorFn = require('getIteratorFn');
@@ -171,13 +172,14 @@ function validateChildKeys(node, parentType) {
172
/**
173
* Assert that the props are valid
174
*
175
+ * @param {object} element
176
* @param {string} componentName Name of the component for error messages.
177
* @param {object} propTypes Map of prop name to a ReactPropType
176
- * @param {object} props
178
* @param {string} location e.g. "prop", "context", "child context"
179
* @private
180
*/
180
-function checkPropTypes(componentName, propTypes, props, location) {
181
+function checkPropTypes(element, componentName, propTypes, location) {
182
+ var props = element.props;
183
for (var propName in propTypes) {
184
if (propTypes.hasOwnProperty(propName)) {
185
var error;
@@ -216,8 +218,12 @@ function checkPropTypes(componentName, propTypes, props, location) {
218
// same error.
219
loggedTypeFailures[error.message] = true;
220
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
+ );
227
}
228
}
229
}
@@ -237,9 +243,9 @@ function validatePropTypes(element) {
243
var name = componentClass.displayName || componentClass.name;
244
if (componentClass.propTypes) {
245
checkPropTypes(
246
+ element,
247
name,
248
componentClass.propTypes,
242
- element.props,
249
ReactPropTypeLocations.prop
250
);
251
}
src/isomorphic/classic/element/__tests__/ReactElementClone-test.js
+4
-1
@@ -271,7 +271,10 @@ describe('ReactElementClone', function() {
271
expect(console.error.argsForCall[0][0]).toBe(
272
'Warning: Failed propType: ' +
273
'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'
278
);
279
});
280
src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js
+11
-5
@@ -242,7 +242,9 @@ describe('ReactElementValidator', function() {
242
expect(console.error.argsForCall[0][0]).toBe(
243
'Warning: Failed propType: ' +
244
'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'
248
);
249
});
250
@@ -318,7 +320,8 @@ describe('ReactElementValidator', function() {
320
expect(console.error.calls.length).toBe(1);
321
expect(console.error.argsForCall[0][0]).toBe(
322
'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'
325
);
326
});
327
@@ -342,7 +345,8 @@ describe('ReactElementValidator', function() {
345
expect(console.error.calls.length).toBe(1);
346
expect(console.error.argsForCall[0][0]).toBe(
347
'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'
350
);
351
});
352
@@ -368,13 +372,15 @@ describe('ReactElementValidator', function() {
372
expect(console.error.calls.length).toBe(2);
373
expect(console.error.argsForCall[0][0]).toBe(
374
'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'
377
);
378
379
expect(console.error.argsForCall[1][0]).toBe(
380
'Warning: Failed propType: ' +
381
'Invalid prop `prop` of type `number` supplied to ' +
377
- '`Component`, expected `string`.'
382
+ '`Component`, expected `string`.\n' +
383
+ ' in Component'
384
);
385
386
ReactTestUtils.renderIntoDocument(
src/isomorphic/classic/types/__tests__/ReactPropTypes-test.js
+5
-2
@@ -891,8 +891,11 @@ describe('ReactPropTypes', function() {
891
var instance = <Component num={6} />;
892
instance = ReactTestUtils.renderIntoDocument(instance);
893
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 **)'
899
);
900
});
901
src/isomorphic/devtools/ReactComponentTreeDevtool.js
+70
-3
@@ -11,6 +11,8 @@
11
12
'use strict';
13
14
+var ReactCurrentOwner = require('ReactCurrentOwner');
15
+
16
var invariant = require('invariant');
17
18
var tree = {};
@@ -53,7 +55,6 @@ var ReactComponentTreeDevtool = {
55
56
onSetChildren(id, nextChildIDs) {
57
updateTree(id, item => {
56
- var prevChildIDs = item.childIDs;
58
item.childIDs = nextChildIDs;
59
60
nextChildIDs.forEach(nextChildID => {
@@ -78,10 +79,20 @@ var ReactComponentTreeDevtool = {
79
'Expected onMountComponent() to fire for the child ' +
80
'before its parent includes it in onSetChildren().'
81
);
81
-
82
- if (prevChildIDs.indexOf(nextChildID) === -1) {
82
+ if (nextChild.parentID == null) {
83
nextChild.parentID = id;
84
+ // TODO: This shouldn't be necessary but mounting a new root during in
85
+ // componentWillMount currently causes not-yet-mounted components to
86
+ // be purged from our tree data so their parent ID is missing.
87
}
88
+ invariant(
89
+ nextChild.parentID === id,
90
+ 'Expected onSetParent() and onSetChildren() to be consistent (%s ' +
91
+ 'has parents %s and %s).',
92
+ nextChildID,
93
+ nextChild.parentID,
94
+ id
95
+ );
96
});
97
});
98
},
@@ -90,6 +101,10 @@ var ReactComponentTreeDevtool = {
101
updateTree(id, item => item.ownerID = ownerID);
102
},
103
104
+ onSetParent(id, parentID) {
105
+ updateTree(id, item => item.parentID = parentID);
106
+ },
107
+
108
onSetText(id, text) {
109
updateTree(id, item => item.text = text);
110
},
@@ -138,6 +153,53 @@ var ReactComponentTreeDevtool = {
153
return item ? item.isMounted : false;
154
},
155
156
+ getCurrentStackAddendum(topElement) {
157
+ function describeComponentFrame(name, source, ownerName) {
158
+ return '\n in ' + name + (
159
+ source ?
160
+ ' (at ' + source.fileName.replace(/^.*[\\\/]/, '') + ':' +
161
+ source.lineNumber + ')' :
162
+ ownerName ?
163
+ ' (created by ' + ownerName + ')' :
164
+ ''
165
+ );
166
+ }
167
+
168
+ function describeID(id) {
169
+ var name = ReactComponentTreeDevtool.getDisplayName(id);
170
+ var element = ReactComponentTreeDevtool.getElement(id);
171
+ var ownerID = ReactComponentTreeDevtool.getOwnerID(id);
172
+ var ownerName;
173
+ if (ownerID) {
174
+ ownerName = ReactComponentTreeDevtool.getDisplayName(ownerID);
175
+ }
176
+ return describeComponentFrame(name, element._source, ownerName);
177
+ }
178
+
179
+ var info = '';
180
+ if (topElement) {
181
+ var type = topElement.type;
182
+ var name = typeof type === 'function' ?
183
+ type.displayName || type.name :
184
+ type;
185
+ var owner = topElement._owner;
186
+ info += describeComponentFrame(
187
+ name || 'Unknown',
188
+ topElement._source,
189
+ owner && owner.getName()
190
+ );
191
+ }
192
+
193
+ var currentOwner = ReactCurrentOwner.current;
194
+ var id = currentOwner && currentOwner._debugID;
195
+ while (id) {
196
+ info += describeID(id);
197
+ id = ReactComponentTreeDevtool.getParentID(id);
198
+ }
199
+
200
+ return info;
201
+ },
202
+
203
getChildIDs(id) {
204
var item = tree[id];
205
return item ? item.childIDs : [];
@@ -148,6 +210,11 @@ var ReactComponentTreeDevtool = {
210
return item ? item.displayName : 'Unknown';
211
},
212
213
+ getElement(id) {
214
+ var item = tree[id];
215
+ return item ? item.element : null;
216
+ },
217
+
218
getOwnerID(id) {
219
var item = tree[id];
220
return item ? item.ownerID : null;
src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js
+74
@@ -1741,4 +1741,78 @@ describe('ReactComponentTreeDevtool', () => {
1741
expect(getRootDisplayNames()).toEqual([]);
1742
expect(getRegisteredDisplayNames()).toEqual([]);
1743
});
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
+ });
1818
});
src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js
+26
-10
@@ -195,10 +195,14 @@ describe('ReactJSXElementValidator', function() {
195
}
196
}
197
ReactTestUtils.renderIntoDocument(<ParentComp />);
198
- expect(console.error.argsForCall[0][0]).toBe(
198
+ expect(
199
+ console.error.argsForCall[0][0].replace(/\(at .+?:\d+\)/g, '(at **)')
200
+ ).toBe(
201
'Warning: Failed propType: ' +
202
'Invalid prop `color` of type `number` supplied to `MyComp`, ' +
201
- 'expected `string`. Check the render method of `ParentComp`.'
203
+ 'expected `string`.\n' +
204
+ ' in MyComp (at **)\n' +
205
+ ' in ParentComp (at **)'
206
);
207
});
208
@@ -242,9 +246,12 @@ describe('ReactJSXElementValidator', function() {
246
ReactTestUtils.renderIntoDocument(<RequiredPropComponent />);
247
248
expect(console.error.calls.length).toBe(1);
245
- expect(console.error.argsForCall[0][0]).toBe(
249
+ expect(
250
+ console.error.argsForCall[0][0].replace(/\(at .+?:\d+\)/g, '(at **)')
251
+ ).toBe(
252
'Warning: Failed propType: ' +
247
- 'Required prop `prop` was not specified in `RequiredPropComponent`.'
253
+ 'Required prop `prop` was not specified in `RequiredPropComponent`.\n' +
254
+ ' in RequiredPropComponent (at **)'
255
);
256
});
257
@@ -254,9 +261,12 @@ describe('ReactJSXElementValidator', function() {
261
ReactTestUtils.renderIntoDocument(<RequiredPropComponent prop={null} />);
262
263
expect(console.error.calls.length).toBe(1);
257
- expect(console.error.argsForCall[0][0]).toBe(
264
+ expect(
265
+ console.error.argsForCall[0][0].replace(/\(at .+?:\d+\)/g, '(at **)')
266
+ ).toBe(
267
'Warning: Failed propType: ' +
259
- 'Required prop `prop` was not specified in `RequiredPropComponent`.'
268
+ 'Required prop `prop` was not specified in `RequiredPropComponent`.\n' +
269
+ ' in RequiredPropComponent (at **)'
270
);
271
});
272
@@ -267,15 +277,21 @@ describe('ReactJSXElementValidator', function() {
277
ReactTestUtils.renderIntoDocument(<RequiredPropComponent prop={42} />);
278
279
expect(console.error.calls.length).toBe(2);
270
- expect(console.error.argsForCall[0][0]).toBe(
280
+ expect(
281
+ console.error.argsForCall[0][0].replace(/\(at .+?:\d+\)/g, '(at **)')
282
+ ).toBe(
283
'Warning: Failed propType: ' +
272
- 'Required prop `prop` was not specified in `RequiredPropComponent`.'
284
+ 'Required prop `prop` was not specified in `RequiredPropComponent`.\n' +
285
+ ' in RequiredPropComponent (at **)'
286
);
287
275
- expect(console.error.argsForCall[1][0]).toBe(
288
+ expect(
289
+ console.error.argsForCall[1][0].replace(/\(at .+?:\d+\)/g, '(at **)')
290
+ ).toBe(
291
'Warning: Failed propType: ' +
292
'Invalid prop `prop` of type `number` supplied to ' +
278
- '`RequiredPropComponent`, expected `string`.'
293
+ '`RequiredPropComponent`, expected `string`.\n' +
294
+ ' in RequiredPropComponent (at **)'
295
);
296
297
ReactTestUtils.renderIntoDocument(<RequiredPropComponent prop="string" />);
src/renderers/dom/shared/ReactDOMComponent.js
+2
@@ -253,6 +253,8 @@ if (__DEV__) {
253
var contentDebugID = debugID + '#text';
254
this._contentDebugID = contentDebugID;
255
ReactInstrumentation.debugTool.onSetDisplayName(contentDebugID, '#text');
256
+ ReactInstrumentation.debugTool.onSetParent(contentDebugID, debugID);
257
+ ReactInstrumentation.debugTool.onBeforeMountComponent(contentDebugID);
258
ReactInstrumentation.debugTool.onSetText(contentDebugID, '' + contentToUse);
259
ReactInstrumentation.debugTool.onMountComponent(contentDebugID);
260
ReactInstrumentation.debugTool.onSetChildren(debugID, [contentDebugID]);
src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
+24
-10
@@ -471,12 +471,21 @@ var ReactCompositeComponentMixin = {
471
}
472
473
this._renderedNodeType = ReactNodeTypes.getType(renderedElement);
474
- this._renderedComponent = this._instantiateReactComponent(
474
+ var child = this._instantiateReactComponent(
475
renderedElement
476
);
477
+ this._renderedComponent = child;
478
+ if (__DEV__) {
479
+ if (child._debugID !== 0 && this._debugID !== 0) {
480
+ ReactInstrumentation.debugTool.onSetParent(
481
+ child._debugID,
482
+ this._debugID
483
+ );
484
+ }
485
+ }
486
487
var markup = ReactReconciler.mountComponent(
479
- this._renderedComponent,
488
+ child,
489
transaction,
490
hostParent,
491
hostContainerInfo,
@@ -487,9 +496,7 @@ var ReactCompositeComponentMixin = {
496
if (this._debugID !== 0) {
497
ReactInstrumentation.debugTool.onSetChildren(
498
this._debugID,
490
- this._renderedComponent._debugID !== 0 ?
491
- [this._renderedComponent._debugID] :
492
- []
499
+ child._debugID !== 0 ? [child._debugID] : []
500
);
501
}
502
}
@@ -1031,12 +1038,21 @@ var ReactCompositeComponentMixin = {
1038
ReactReconciler.unmountComponent(prevComponentInstance, false);
1039
1040
this._renderedNodeType = ReactNodeTypes.getType(nextRenderedElement);
1034
- this._renderedComponent = this._instantiateReactComponent(
1041
+ var child = this._instantiateReactComponent(
1042
nextRenderedElement
1043
);
1044
+ this._renderedComponent = child;
1045
+ if (__DEV__) {
1046
+ if (child._debugID !== 0 && this._debugID !== 0) {
1047
+ ReactInstrumentation.debugTool.onSetParent(
1048
+ child._debugID,
1049
+ this._debugID
1050
+ );
1051
+ }
1052
+ }
1053
1054
var nextMarkup = ReactReconciler.mountComponent(
1039
- this._renderedComponent,
1055
+ child,
1056
transaction,
1057
this._hostParent,
1058
this._hostContainerInfo,
@@ -1047,9 +1063,7 @@ var ReactCompositeComponentMixin = {
1063
if (this._debugID !== 0) {
1064
ReactInstrumentation.debugTool.onSetChildren(
1065
this._debugID,
1050
- this._renderedComponent._debugID !== 0 ?
1051
- [this._renderedComponent._debugID] :
1052
- []
1066
+ child._debugID !== 0 ? [child._debugID] : []
1067
);
1068
}
1069
}
src/renderers/shared/stack/reconciler/ReactMultiChild.js
+9
@@ -139,8 +139,14 @@ function processQueue(inst, updateQueue) {
139
);
140
}
141
142
+var setParentForInstrumentation = emptyFunction;
143
var setChildrenForInstrumentation = emptyFunction;
144
if (__DEV__) {
145
+ setParentForInstrumentation = function(child) {
146
+ if (child._debugID !== 0) {
147
+ ReactInstrumentation.debugTool.onSetParent(child._debugID, this._debugID);
148
+ }
149
+ };
150
setChildrenForInstrumentation = function(children) {
151
ReactInstrumentation.debugTool.onSetChildren(
152
this._debugID,
@@ -232,6 +238,9 @@ var ReactMultiChild = {
238
for (var name in children) {
239
if (children.hasOwnProperty(name)) {
240
var child = children[name];
241
+ if (__DEV__) {
242
+ setParentForInstrumentation.call(this, child);
243
+ }
244
var mountImage = ReactReconciler.mountComponent(
245
child,
246
transaction,
src/renderers/shared/stack/reconciler/ReactReconciler.js
+4
@@ -222,6 +222,10 @@ var ReactReconciler = {
222
internalInstance._debugID,
223
'performUpdateIfNecessary'
224
);
225
+ ReactInstrumentation.debugTool.onBeforeUpdateComponent(
226
+ internalInstance._debugID,
227
+ internalInstance._currentElement
228
+ );
229
}
230
}
231
internalInstance.performUpdateIfNecessary(transaction);
src/renderers/shared/stack/reconciler/__tests__/ReactStatelessComponent-test.js
+5
-2
@@ -175,9 +175,12 @@ describe('ReactStatelessComponent', function() {
175
spyOn(console, 'error');
176
ReactTestUtils.renderIntoDocument(<Child />);
177
expect(console.error.argsForCall.length).toBe(1);
178
- expect(console.error.argsForCall[0][0]).toBe(
178
+ expect(
179
+ console.error.argsForCall[0][0].replace(/\(at .+?:\d+\)/g, '(at **)')
180
+ ).toBe(
181
'Warning: Failed propType: Invalid prop `test` of type `number` ' +
180
- 'supplied to `Child`, expected `string`.'
182
+ 'supplied to `Child`, expected `string`.\n' +
183
+ ' in Child (at **)'
184
);
185
});
186