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') {