@samitouri / QOS-React-2 / commits / 378c879a6a

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