From d2039d7facee6cbd96111502b5539092b74d64de Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Wed, 21 Dec 2016 13:17:34 -0800 Subject: [PATCH] Improve error messages for invalid element types (#8612) (cherry picked from commit eca5b1d48e71218800156ce474ec79b990b09fbd) --- .../classic/element/ReactElementValidator.js | 32 +++++++--- .../__tests__/ReactElementValidator-test.js | 58 ++++++++++++------- .../ReactJSXElementValidator-test.js | 29 ++++++---- .../__tests__/ReactComponent-test.js | 23 +++++++- .../reconciler/instantiateReactComponent.js | 35 ++++++++--- 5 files changed, 127 insertions(+), 50 deletions(-) diff --git a/src/isomorphic/classic/element/ReactElementValidator.js b/src/isomorphic/classic/element/ReactElementValidator.js index 8c30884017..e7ad9d72a7 100644 --- a/src/isomorphic/classic/element/ReactElementValidator.js +++ b/src/isomorphic/classic/element/ReactElementValidator.js @@ -187,13 +187,31 @@ var ReactElementValidator = { // We warn in this case but don't throw. We expect the element creation to // succeed and there will likely be errors in render. if (!validType) { - warning( - false, - 'React.createElement: type should not be null, undefined, boolean, or ' + - 'number. It should be a string (for DOM elements) or a ReactClass ' + - '(for composite components).%s', - getDeclarationErrorAddendum() - ); + if ( + typeof type !== 'function' && + typeof type !== 'string' + ) { + var info = ''; + if ( + type === undefined || + typeof type === 'object' && + type !== null && + Object.keys(type).length === 0 + ) { + info += + ' You likely forgot to export your component from the file ' + + 'it\'s defined in.'; + } + info += getDeclarationErrorAddendum(); + warning( + false, + 'React.createElement: type is invalid -- expected a string (for ' + + 'built-in components) or a class/function (for composite ' + + 'components) but got: %s.%s', + type == null ? type : typeof type, + info, + ); + } } var element = ReactElement.createElement.apply(this, arguments); diff --git a/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js b/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js index 5690e2125b..b36fa2f39c 100644 --- a/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js +++ b/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js @@ -289,35 +289,49 @@ describe('ReactElementValidator', () => { ); }); - it('gives a helpful error when passing null, undefined, boolean, or number', () => { + it('gives a helpful error when passing invalid types', () => { spyOn(console, 'error'); React.createElement(undefined); React.createElement(null); React.createElement(true); React.createElement(123); - expect(console.error.calls.count()).toBe(4); + React.createElement({x: 17}); + React.createElement({}); + expect(console.error.calls.count()).toBe(6); expect(console.error.calls.argsFor(0)[0]).toBe( - 'Warning: React.createElement: type should not be null, undefined, ' + - 'boolean, or number. It should be a string (for DOM elements) or a ' + - 'ReactClass (for composite components).' + 'Warning: React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: undefined. You likely forgot to export your ' + + 'component from the file it\'s defined in.' ); expect(console.error.calls.argsFor(1)[0]).toBe( - 'Warning: React.createElement: type should not be null, undefined, ' + - 'boolean, or number. It should be a string (for DOM elements) or a ' + - 'ReactClass (for composite components).' + 'Warning: React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: null.' ); expect(console.error.calls.argsFor(2)[0]).toBe( - 'Warning: React.createElement: type should not be null, undefined, ' + - 'boolean, or number. It should be a string (for DOM elements) or a ' + - 'ReactClass (for composite components).' + 'Warning: React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: boolean.' ); expect(console.error.calls.argsFor(3)[0]).toBe( - 'Warning: React.createElement: type should not be null, undefined, ' + - 'boolean, or number. It should be a string (for DOM elements) or a ' + - 'ReactClass (for composite components).' + 'Warning: React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: number.' + ); + expect(console.error.calls.argsFor(4)[0]).toBe( + 'Warning: React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: object.' + ); + expect(console.error.calls.argsFor(5)[0]).toBe( + 'Warning: React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: object. You likely forgot to export your ' + + 'component from the file it\'s defined in.' ); React.createElement('div'); - expect(console.error.calls.count()).toBe(4); + expect(console.error.calls.count()).toBe(6); }); it('includes the owner name when passing null, undefined, boolean, or number', () => { @@ -336,10 +350,9 @@ describe('ReactElementValidator', () => { ); expect(console.error.calls.count()).toBe(1); expect(console.error.calls.argsFor(0)[0]).toBe( - 'Warning: React.createElement: type should not be null, undefined, ' + - 'boolean, or number. It should be a string (for DOM elements) or a ' + - 'ReactClass (for composite components). Check the render method of ' + - '`ParentComp`.' + 'Warning: React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: null. Check the render method of `ParentComp`.' ); }); @@ -537,9 +550,10 @@ describe('ReactElementValidator', () => { void {[
]}; expect(console.error.calls.count()).toBe(1); expect(console.error.calls.argsFor(0)[0]).toBe( - 'Warning: React.createElement: type should not be null, undefined, ' + - 'boolean, or number. It should be a string (for DOM elements) or a ' + - 'ReactClass (for composite components).' + 'Warning: React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: undefined. You likely forgot to export your ' + + 'component from the file it\'s defined in.' ); }); diff --git a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js index 71fbed5bd3..00f3650217 100644 --- a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js +++ b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js @@ -218,21 +218,26 @@ describe('ReactJSXElementValidator', () => { void ; void ; expect(console.error.calls.count()).toBe(4); - expect(console.error.calls.argsFor(0)[0]).toContain( - 'type should not be null, undefined, boolean, or number. It should be ' + - 'a string (for DOM elements) or a ReactClass (for composite components).' + expect(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: undefined. You likely forgot to export your ' + + 'component from the file it\'s defined in.' ); - expect(console.error.calls.argsFor(1)[0]).toContain( - 'type should not be null, undefined, boolean, or number. It should be ' + - 'a string (for DOM elements) or a ReactClass (for composite components).' + expect(console.error.calls.argsFor(1)[0]).toBe( + 'Warning: React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: null.' ); - expect(console.error.calls.argsFor(2)[0]).toContain( - 'type should not be null, undefined, boolean, or number. It should be ' + - 'a string (for DOM elements) or a ReactClass (for composite components).' + expect(console.error.calls.argsFor(2)[0]).toBe( + 'Warning: React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: boolean.' ); - expect(console.error.calls.argsFor(3)[0]).toContain( - 'type should not be null, undefined, boolean, or number. It should be ' + - 'a string (for DOM elements) or a ReactClass (for composite components).' + expect(console.error.calls.argsFor(3)[0]).toBe( + 'Warning: React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: number.' ); void
; expect(console.error.calls.count()).toBe(4); diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactComponent-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactComponent-test.js index 769b2bafcc..28c914152b 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactComponent-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactComponent-test.js @@ -327,7 +327,9 @@ describe('ReactComponent', () => { var X = undefined; expect(() => ReactTestUtils.renderIntoDocument()).toThrowError( 'Element type is invalid: expected a string (for built-in components) ' + - 'or a class/function (for composite components) but got: undefined.' + 'or a class/function (for composite components) but got: undefined. ' + + 'You likely forgot to export your component from the file it\'s ' + + 'defined in.' ); var Y = null; @@ -340,4 +342,23 @@ describe('ReactComponent', () => { expect(console.error.calls.count()).toBe(2); }); + it('includes owner name in the error about badly-typed elements', () => { + spyOn(console, 'error'); + + function Foo() { + var X = undefined; + return ; + } + + expect(() => ReactTestUtils.renderIntoDocument()).toThrowError( + 'Element type is invalid: expected a string (for built-in components) ' + + 'or a class/function (for composite components) but got: undefined. ' + + 'You likely forgot to export your component from the file it\'s ' + + 'defined in. Check the render method of `Foo`.' + ); + + // One warning for each element creation + expect(console.error.calls.count()).toBe(1); + }); + }); diff --git a/src/renderers/shared/stack/reconciler/instantiateReactComponent.js b/src/renderers/shared/stack/reconciler/instantiateReactComponent.js index 202768d8bb..d1b5c23262 100644 --- a/src/renderers/shared/stack/reconciler/instantiateReactComponent.js +++ b/src/renderers/shared/stack/reconciler/instantiateReactComponent.js @@ -72,14 +72,33 @@ function instantiateReactComponent(node, shouldHaveDebugID) { instance = ReactEmptyComponent.create(instantiateReactComponent); } else if (typeof node === 'object') { var element = node; - invariant( - element && (typeof element.type === 'function' || - typeof element.type === 'string'), - 'Element type is invalid: expected a string (for built-in components) ' + - 'or a class/function (for composite components) but got: %s.%s', - element.type == null ? element.type : typeof element.type, - getDeclarationErrorAddendum(element._owner) - ); + var type = element.type; + if ( + typeof type !== 'function' && + typeof type !== 'string' + ) { + var info = ''; + if (__DEV__) { + if ( + type === undefined || + typeof type === 'object' && + type !== null && + Object.keys(type).length === 0 + ) { + info += + ' You likely forgot to export your component from the file ' + + 'it\'s defined in.'; + } + } + info += getDeclarationErrorAddendum(element._owner); + invariant( + false, + 'Element type is invalid: expected a string (for built-in components) ' + + 'or a class/function (for composite components) but got: %s.%s', + type == null ? type : typeof type, + info, + ); + } // Special case string values if (typeof element.type === 'string') {