Skip to content

Commit 5c415d1

Browse files
authored
Provide non-standard stack with invalid type warnings (#9679)
* Provide non-standard stack with invalid type warnings * Include parent stack but mark owner chain as pertinent * Just parent stack is enough for my needs Because to avoid noise it is enough to collapse too close frames in the UI. * functionName => name * Hide behind a feature flag
1 parent 2bbe024 commit 5c415d1

File tree

5 files changed

+162
-0
lines changed

5 files changed

+162
-0
lines changed

.flowconfig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
<PROJECT_ROOT>/examples/.*
44
<PROJECT_ROOT>/fixtures/.*
55
<PROJECT_ROOT>/build/.*
6+
<PROJECT_ROOT>/node_modules/chrome-devtools-frontend/.*
7+
<PROJECT_ROOT>/.*/node_modules/chrome-devtools-frontend/.*
68
<PROJECT_ROOT>/.*/node_modules/y18n/.*
79
<PROJECT_ROOT>/.*/__mocks__/.*
810
<PROJECT_ROOT>/.*/__tests__/.*

src/isomorphic/classic/element/ReactElementValidator.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,12 @@ var ReactElementValidator = {
223223

224224
info += ReactComponentTreeHook.getCurrentStackAddendum();
225225

226+
var currentSource = props !== null &&
227+
props !== undefined &&
228+
props.__source !== undefined
229+
? props.__source
230+
: null;
231+
ReactComponentTreeHook.pushNonStandardWarningStack(true, currentSource);
226232
warning(
227233
false,
228234
'React.createElement: type is invalid -- expected a string (for ' +
@@ -231,6 +237,7 @@ var ReactElementValidator = {
231237
type == null ? type : typeof type,
232238
info,
233239
);
240+
ReactComponentTreeHook.popNonStandardWarningStack();
234241
}
235242
}
236243

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,4 +525,55 @@ describe('ReactElementValidator', () => {
525525
"component from the file it's defined in. Check your code at **.",
526526
);
527527
});
528+
529+
it('provides stack via non-standard console.reactStack for invalid types', () => {
530+
spyOn(console, 'error');
531+
532+
function Foo() {
533+
var Bad = undefined;
534+
return React.createElement(Bad);
535+
}
536+
537+
function App() {
538+
return React.createElement('div', null, React.createElement(Foo));
539+
}
540+
541+
try {
542+
console.reactStack = jest.fn();
543+
console.reactStackEnd = jest.fn();
544+
545+
expect(() => {
546+
ReactTestUtils.renderIntoDocument(React.createElement(App));
547+
}).toThrow(
548+
'Element type is invalid: expected a string (for built-in components) ' +
549+
'or a class/function (for composite components) but got: undefined. ' +
550+
"You likely forgot to export your component from the file it's " +
551+
'defined in. Check the render method of `Foo`.',
552+
);
553+
554+
expect(console.reactStack.mock.calls.length).toBe(1);
555+
expect(console.reactStackEnd.mock.calls.length).toBe(1);
556+
557+
var stack = console.reactStack.mock.calls[0][0];
558+
expect(Array.isArray(stack)).toBe(true);
559+
expect(stack.map(frame => frame.name)).toEqual([
560+
'Foo', // <Bad> is inside Foo
561+
'App', // <Foo> is inside App
562+
'App', // <div> is inside App
563+
null, // <App> is outside a component
564+
]);
565+
expect(
566+
stack.map(frame => frame.fileName && frame.fileName.slice(-8)),
567+
).toEqual([null, null, null, null]);
568+
expect(stack.map(frame => frame.lineNumber)).toEqual([
569+
null,
570+
null,
571+
null,
572+
null,
573+
]);
574+
} finally {
575+
delete console.reactStack;
576+
delete console.reactStackEnd;
577+
}
578+
});
528579
});

src/isomorphic/hooks/ReactComponentTreeHook.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,57 @@ var ReactComponentTreeHook = {
402402

403403
getRootIDs,
404404
getRegisteredIDs: getItemIDs,
405+
406+
pushNonStandardWarningStack(
407+
isCreatingElement: boolean,
408+
currentSource: ?Source,
409+
) {
410+
if (typeof console.reactStack !== 'function') {
411+
return;
412+
}
413+
414+
var stack = [];
415+
var currentOwner = ReactCurrentOwner.current;
416+
var id = currentOwner && currentOwner._debugID;
417+
418+
try {
419+
if (isCreatingElement) {
420+
stack.push({
421+
name: id ? ReactComponentTreeHook.getDisplayName(id) : null,
422+
fileName: currentSource ? currentSource.fileName : null,
423+
lineNumber: currentSource ? currentSource.lineNumber : null,
424+
});
425+
}
426+
427+
while (id) {
428+
var element = ReactComponentTreeHook.getElement(id);
429+
var parentID = ReactComponentTreeHook.getParentID(id);
430+
var ownerID = ReactComponentTreeHook.getOwnerID(id);
431+
var ownerName = ownerID
432+
? ReactComponentTreeHook.getDisplayName(ownerID)
433+
: null;
434+
var source = element && element._source;
435+
stack.push({
436+
name: ownerName,
437+
fileName: source ? source.fileName : null,
438+
lineNumber: source ? source.lineNumber : null,
439+
});
440+
id = parentID;
441+
}
442+
} catch (err) {
443+
// Internal state is messed up.
444+
// Stop building the stack (it's just a nice to have).
445+
}
446+
447+
console.reactStack(stack);
448+
},
449+
450+
popNonStandardWarningStack() {
451+
if (typeof console.reactStackEnd !== 'function') {
452+
return;
453+
}
454+
console.reactStackEnd();
455+
},
405456
};
406457

407458
module.exports = ReactComponentTreeHook;

src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,4 +400,55 @@ describe('ReactJSXElementValidator', () => {
400400
' Use a static property named `defaultProps` instead.',
401401
);
402402
});
403+
404+
it('provides stack via non-standard console.reactStack for invalid types', () => {
405+
spyOn(console, 'error');
406+
407+
function Foo() {
408+
var Bad = undefined;
409+
return <Bad />;
410+
}
411+
412+
function App() {
413+
return <div><Foo /></div>;
414+
}
415+
416+
try {
417+
console.reactStack = jest.fn();
418+
console.reactStackEnd = jest.fn();
419+
420+
expect(() => {
421+
ReactTestUtils.renderIntoDocument(<App />);
422+
}).toThrow(
423+
'Element type is invalid: expected a string (for built-in components) ' +
424+
'or a class/function (for composite components) but got: undefined. ' +
425+
"You likely forgot to export your component from the file it's " +
426+
'defined in. Check the render method of `Foo`.',
427+
);
428+
429+
expect(console.reactStack.mock.calls.length).toBe(1);
430+
expect(console.reactStackEnd.mock.calls.length).toBe(1);
431+
432+
var stack = console.reactStack.mock.calls[0][0];
433+
expect(Array.isArray(stack)).toBe(true);
434+
expect(stack.map(frame => frame.name)).toEqual([
435+
'Foo', // <Bad> is inside Foo
436+
'App', // <Foo> is inside App
437+
'App', // <div> is inside App
438+
null, // <App> is outside a component
439+
]);
440+
expect(
441+
stack.map(frame => frame.fileName && frame.fileName.slice(-8)),
442+
).toEqual(['-test.js', '-test.js', '-test.js', '-test.js']);
443+
expect(stack.map(frame => typeof frame.lineNumber)).toEqual([
444+
'number',
445+
'number',
446+
'number',
447+
'number',
448+
]);
449+
} finally {
450+
delete console.reactStack;
451+
delete console.reactStackEnd;
452+
}
453+
});
403454
});

0 commit comments

Comments
 (0)