@samitouri / QOS-React / commits / eca5b1d48e

Improve error messages for invalid element types (#8612)

Ben Alpert committed Dec 21, 2016 at 13:17 UTC eca5b1d48e71218800156ce474ec79b990b09fbd
8 files changed +141 -58
scripts/fiber/tests-passing.txt
+1 -1
@@ -286,7 +286,7 @@ src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js
286 * does not warn when the element is directly in rest args
287 * does not warn when the array contains a non-element
288 * should give context for PropType errors in nested components.
289 -* gives a helpful error when passing null, undefined, boolean, or number
289 +* gives a helpful error when passing invalid types
290 * should check default prop values
291 * should not check the default for explicit null
292 * should check declared prop types
src/isomorphic/classic/element/ReactElementValidator.js
+25 -7
@@ -191,13 +191,31 @@ var ReactElementValidator = {
191 // We warn in this case but don't throw. We expect the element creation to
192 // succeed and there will likely be errors in render.
193 if (!validType) {
194 - warning(
195 - false,
196 - 'React.createElement: type should not be null, undefined, boolean, or ' +
197 - 'number. It should be a string (for DOM elements) or a ReactClass ' +
198 - '(for composite components).%s',
199 - getDeclarationErrorAddendum()
200 - );
194 + if (
195 + typeof type !== 'function' &&
196 + typeof type !== 'string'
197 + ) {
198 + var info = '';
199 + if (
200 + type === undefined ||
201 + typeof type === 'object' &&
202 + type !== null &&
203 + Object.keys(type).length === 0
204 + ) {
205 + info +=
206 + ' You likely forgot to export your component from the file ' +
207 + 'it\'s defined in.';
208 + }
209 + info += getDeclarationErrorAddendum();
210 + warning(
211 + false,
212 + 'React.createElement: type is invalid -- expected a string (for ' +
213 + 'built-in components) or a class/function (for composite ' +
214 + 'components) but got: %s.%s',
215 + type == null ? type : typeof type,
216 + info,
217 + );
218 + }
219 }
220
221 var element = ReactElement.createElement.apply(this, arguments);
src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js
+36 -22
@@ -286,35 +286,49 @@ describe('ReactElementValidator', () => {
286 );
287 });
288
289 - it('gives a helpful error when passing null, undefined, boolean, or number', () => {
289 + it('gives a helpful error when passing invalid types', () => {
290 spyOn(console, 'error');
291 React.createElement(undefined);
292 React.createElement(null);
293 React.createElement(true);
294 React.createElement(123);
295 - expectDev(console.error.calls.count()).toBe(4);
295 + React.createElement({x: 17});
296 + React.createElement({});
297 + expectDev(console.error.calls.count()).toBe(6);
298 expectDev(console.error.calls.argsFor(0)[0]).toBe(
297 - 'Warning: React.createElement: type should not be null, undefined, ' +
298 - 'boolean, or number. It should be a string (for DOM elements) or a ' +
299 - 'ReactClass (for composite components).'
299 + 'Warning: React.createElement: type is invalid -- expected a string ' +
300 + '(for built-in components) or a class/function (for composite ' +
301 + 'components) but got: undefined. You likely forgot to export your ' +
302 + 'component from the file it\'s defined in.'
303 );
304 expectDev(console.error.calls.argsFor(1)[0]).toBe(
302 - 'Warning: React.createElement: type should not be null, undefined, ' +
303 - 'boolean, or number. It should be a string (for DOM elements) or a ' +
304 - 'ReactClass (for composite components).'
305 + 'Warning: React.createElement: type is invalid -- expected a string ' +
306 + '(for built-in components) or a class/function (for composite ' +
307 + 'components) but got: null.'
308 );
309 expectDev(console.error.calls.argsFor(2)[0]).toBe(
307 - 'Warning: React.createElement: type should not be null, undefined, ' +
308 - 'boolean, or number. It should be a string (for DOM elements) or a ' +
309 - 'ReactClass (for composite components).'
310 + 'Warning: React.createElement: type is invalid -- expected a string ' +
311 + '(for built-in components) or a class/function (for composite ' +
312 + 'components) but got: boolean.'
313 );
314 expectDev(console.error.calls.argsFor(3)[0]).toBe(
312 - 'Warning: React.createElement: type should not be null, undefined, ' +
313 - 'boolean, or number. It should be a string (for DOM elements) or a ' +
314 - 'ReactClass (for composite components).'
315 + 'Warning: React.createElement: type is invalid -- expected a string ' +
316 + '(for built-in components) or a class/function (for composite ' +
317 + 'components) but got: number.'
318 + );
319 + expectDev(console.error.calls.argsFor(4)[0]).toBe(
320 + 'Warning: React.createElement: type is invalid -- expected a string ' +
321 + '(for built-in components) or a class/function (for composite ' +
322 + 'components) but got: object.'
323 + );
324 + expectDev(console.error.calls.argsFor(5)[0]).toBe(
325 + 'Warning: React.createElement: type is invalid -- expected a string ' +
326 + '(for built-in components) or a class/function (for composite ' +
327 + 'components) but got: object. You likely forgot to export your ' +
328 + 'component from the file it\'s defined in.'
329 );
330 React.createElement('div');
317 - expectDev(console.error.calls.count()).toBe(4);
331 + expectDev(console.error.calls.count()).toBe(6);
332 });
333
334 it('includes the owner name when passing null, undefined, boolean, or number', () => {
@@ -333,10 +347,9 @@ describe('ReactElementValidator', () => {
347 );
348 expectDev(console.error.calls.count()).toBe(1);
349 expectDev(console.error.calls.argsFor(0)[0]).toBe(
336 - 'Warning: React.createElement: type should not be null, undefined, ' +
337 - 'boolean, or number. It should be a string (for DOM elements) or a ' +
338 - 'ReactClass (for composite components). Check the render method of ' +
339 - '`ParentComp`.'
350 + 'Warning: React.createElement: type is invalid -- expected a string ' +
351 + '(for built-in components) or a class/function (for composite ' +
352 + 'components) but got: null. Check the render method of `ParentComp`.'
353 );
354 });
355
@@ -534,9 +547,10 @@ describe('ReactElementValidator', () => {
547 void <Foo>{[<div />]}</Foo>;
548 expectDev(console.error.calls.count()).toBe(1);
549 expectDev(console.error.calls.argsFor(0)[0]).toBe(
537 - 'Warning: React.createElement: type should not be null, undefined, ' +
538 - 'boolean, or number. It should be a string (for DOM elements) or a ' +
539 - 'ReactClass (for composite components).'
550 + 'Warning: React.createElement: type is invalid -- expected a string ' +
551 + '(for built-in components) or a class/function (for composite ' +
552 + 'components) but got: undefined. You likely forgot to export your ' +
553 + 'component from the file it\'s defined in.'
554 );
555 });
556
src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js
+17 -12
@@ -256,21 +256,26 @@ describe('ReactJSXElementValidator', () => {
256 void <True />;
257 void <Num />;
258 expectDev(console.error.calls.count()).toBe(4);
259 - expectDev(console.error.calls.argsFor(0)[0]).toContain(
260 - 'type should not be null, undefined, boolean, or number. It should be ' +
261 - 'a string (for DOM elements) or a ReactClass (for composite components).'
259 + expectDev(console.error.calls.argsFor(0)[0]).toBe(
260 + 'Warning: React.createElement: type is invalid -- expected a string ' +
261 + '(for built-in components) or a class/function (for composite ' +
262 + 'components) but got: undefined. You likely forgot to export your ' +
263 + 'component from the file it\'s defined in.'
264 );
263 - expectDev(console.error.calls.argsFor(1)[0]).toContain(
264 - 'type should not be null, undefined, boolean, or number. It should be ' +
265 - 'a string (for DOM elements) or a ReactClass (for composite components).'
265 + expectDev(console.error.calls.argsFor(1)[0]).toBe(
266 + 'Warning: React.createElement: type is invalid -- expected a string ' +
267 + '(for built-in components) or a class/function (for composite ' +
268 + 'components) but got: null.'
269 );
267 - expectDev(console.error.calls.argsFor(2)[0]).toContain(
268 - 'type should not be null, undefined, boolean, or number. It should be ' +
269 - 'a string (for DOM elements) or a ReactClass (for composite components).'
270 + expectDev(console.error.calls.argsFor(2)[0]).toBe(
271 + 'Warning: React.createElement: type is invalid -- expected a string ' +
272 + '(for built-in components) or a class/function (for composite ' +
273 + 'components) but got: boolean.'
274 );
271 - expectDev(console.error.calls.argsFor(3)[0]).toContain(
272 - 'type should not be null, undefined, boolean, or number. It should be ' +
273 - 'a string (for DOM elements) or a ReactClass (for composite components).'
275 + expectDev(console.error.calls.argsFor(3)[0]).toBe(
276 + 'Warning: React.createElement: type is invalid -- expected a string ' +
277 + '(for built-in components) or a class/function (for composite ' +
278 + 'components) but got: number.'
279 );
280 void <Div />;
281 expectDev(console.error.calls.count()).toBe(4);
src/renderers/shared/fiber/ReactFiber.js
+21 -3
@@ -334,7 +334,11 @@ function createFiberFromElementType(type : mixed, key : null | string) : Fiber {
334 } else if (typeof type === 'string') {
335 fiber = createFiber(HostComponent, key);
336 fiber.type = type;
337 - } else if (typeof type === 'object' && type !== null) {
337 + } else if (
338 + typeof type === 'object' &&
339 + type !== null &&
340 + typeof type.tag === 'number'
341 + ) {
342 // Currently assumed to be a continuation and therefore is a fiber already.
343 // TODO: The yield system is currently broken for updates in some cases.
344 // The reified yield stores a fiber, but we don't know which fiber that is;
@@ -343,12 +347,26 @@ function createFiberFromElementType(type : mixed, key : null | string) : Fiber {
347 // There is probably a clever way to restructure this.
348 fiber = ((type : any) : Fiber);
349 } else {
350 + let info = '';
351 + if (__DEV__) {
352 + if (
353 + type === undefined ||
354 + typeof type === 'object' &&
355 + type !== null &&
356 + Object.keys(type).length === 0
357 + ) {
358 + info +=
359 + ' You likely forgot to export your component from the file ' +
360 + 'it\'s defined in.';
361 + }
362 + }
363 + // TODO: Stack also includes owner name in the message.
364 invariant(
365 false,
366 'Element type is invalid: expected a string (for built-in components) ' +
349 - 'or a class/function (for composite components) but got: %s.',
367 + 'or a class/function (for composite components) but got: %s.%s',
368 type == null ? type : typeof type,
351 - // TODO: Stack also includes owner name in the message.
369 + info,
370 );
371 }
372 return fiber;
src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
+9 -3
@@ -752,7 +752,9 @@ describe('ReactIncrementalErrorHandling', () => {
752 ReactNoop.flush();
753 expect(ReactNoop.getChildren()).toEqual([span(
754 'Element type is invalid: expected a string (for built-in components) or ' +
755 - 'a class/function (for composite components) but got: undefined.'
755 + 'a class/function (for composite components) but got: undefined. ' +
756 + 'You likely forgot to export your component from the file it\'s ' +
757 + 'defined in.'
758 )]);
759 expect(console.error.calls.count()).toBe(1);
760 });
@@ -794,7 +796,9 @@ describe('ReactIncrementalErrorHandling', () => {
796 ReactNoop.flush();
797 expect(ReactNoop.getChildren()).toEqual([span(
798 'Element type is invalid: expected a string (for built-in components) or ' +
797 - 'a class/function (for composite components) but got: undefined.'
799 + 'a class/function (for composite components) but got: undefined. ' +
800 + 'You likely forgot to export your component from the file it\'s ' +
801 + 'defined in.'
802 )]);
803 expect(console.error.calls.count()).toBe(1);
804 });
@@ -807,7 +811,9 @@ describe('ReactIncrementalErrorHandling', () => {
811 ReactNoop.flush();
812 }).toThrowError(
813 'Element type is invalid: expected a string (for built-in components) or ' +
810 - 'a class/function (for composite components) but got: undefined.'
814 + 'a class/function (for composite components) but got: undefined. ' +
815 + 'You likely forgot to export your component from the file it\'s ' +
816 + 'defined in.'
817 );
818
819 ReactNoop.render(<span prop="hi" />);
src/renderers/shared/shared/__tests__/ReactComponent-test.js
+5 -2
@@ -334,7 +334,9 @@ describe('ReactComponent', () => {
334 var X = undefined;
335 expect(() => ReactTestUtils.renderIntoDocument(<X />)).toThrowError(
336 'Element type is invalid: expected a string (for built-in components) ' +
337 - 'or a class/function (for composite components) but got: undefined.'
337 + 'or a class/function (for composite components) but got: undefined. ' +
338 + 'You likely forgot to export your component from the file it\'s ' +
339 + 'defined in.'
340 );
341
342 var Y = null;
@@ -358,7 +360,8 @@ describe('ReactComponent', () => {
360 expect(() => ReactTestUtils.renderIntoDocument(<Foo />)).toThrowError(
361 'Element type is invalid: expected a string (for built-in components) ' +
362 'or a class/function (for composite components) but got: undefined. ' +
361 - 'Check the render method of `Foo`.'
363 + 'You likely forgot to export your component from the file it\'s ' +
364 + 'defined in. Check the render method of `Foo`.'
365 );
366
367 // One warning for each element creation
src/renderers/shared/stack/reconciler/instantiateReactComponent.js
+27 -8
@@ -72,14 +72,33 @@ function instantiateReactComponent(node, shouldHaveDebugID) {
72 instance = ReactEmptyComponent.create(instantiateReactComponent);
73 } else if (typeof node === 'object') {
74 var element = node;
75 - invariant(
76 - element && (typeof element.type === 'function' ||
77 - typeof element.type === 'string'),
78 - 'Element type is invalid: expected a string (for built-in components) ' +
79 - 'or a class/function (for composite components) but got: %s.%s',
80 - element.type == null ? element.type : typeof element.type,
81 - getDeclarationErrorAddendum(element._owner)
82 - );
75 + var type = element.type;
76 + if (
77 + typeof type !== 'function' &&
78 + typeof type !== 'string'
79 + ) {
80 + var info = '';
81 + if (__DEV__) {
82 + if (
83 + type === undefined ||
84 + typeof type === 'object' &&
85 + type !== null &&
86 + Object.keys(type).length === 0
87 + ) {
88 + info +=
89 + ' You likely forgot to export your component from the file ' +
90 + 'it\'s defined in.';
91 + }
92 + }
93 + info += getDeclarationErrorAddendum(element._owner);
94 + invariant(
95 + false,
96 + 'Element type is invalid: expected a string (for built-in components) ' +
97 + 'or a class/function (for composite components) but got: %s.%s',
98 + type == null ? type : typeof type,
99 + info,
100 + );
101 + }
102
103 // Special case string values
104 if (typeof element.type === 'string') {