Include the function name for context on invalid function child (#28362)
Also warn for symbols. It's weird because for objects we throw a hard error but functions we do a dev only check. Mainly because we have an object branch anyway. In the object branch we have some built-ins that have bad errors like forwardRef and memo but since they're going to become functions later, I didn't bother updating those. Once they're functions those names will be part of this.
Sebastian Markbåge committed
Feb 17, 2024 at 16:41 UTC
c1fd2a91b1042c137d750be85e5998f699a54d2a
5 files changed
+137
-28
packages/react-dom/src/__tests__/ReactComponent-test.js
+10
-5
@@ -635,8 +635,9 @@ describe('ReactComponent', () => {
635
});
636
}).toErrorDev(
637
'Warning: Functions are not valid as a React child. This may happen if ' +
638
- 'you return a Component instead of <Component /> from render. ' +
638
+ 'you return Foo instead of <Foo /> from render. ' +
639
'Or maybe you meant to call this function rather than return it.\n' +
640
+ ' <Foo>{Foo}</Foo>\n' +
641
' in Foo (at **)',
642
);
643
});
@@ -656,8 +657,9 @@ describe('ReactComponent', () => {
657
});
658
}).toErrorDev(
659
'Warning: Functions are not valid as a React child. This may happen if ' +
659
- 'you return a Component instead of <Component /> from render. ' +
660
+ 'you return Foo instead of <Foo /> from render. ' +
661
'Or maybe you meant to call this function rather than return it.\n' +
662
+ ' <Foo>{Foo}</Foo>\n' +
663
' in Foo (at **)',
664
);
665
});
@@ -678,8 +680,9 @@ describe('ReactComponent', () => {
680
});
681
}).toErrorDev(
682
'Warning: Functions are not valid as a React child. This may happen if ' +
681
- 'you return a Component instead of <Component /> from render. ' +
683
+ 'you return Foo instead of <Foo /> from render. ' +
684
'Or maybe you meant to call this function rather than return it.\n' +
685
+ ' <span>{Foo}</span>\n' +
686
' in span (at **)\n' +
687
' in div (at **)\n' +
688
' in Foo (at **)',
@@ -730,13 +733,15 @@ describe('ReactComponent', () => {
733
});
734
}).toErrorDev([
735
'Warning: Functions are not valid as a React child. This may happen if ' +
733
- 'you return a Component instead of <Component /> from render. ' +
736
+ 'you return Foo instead of <Foo /> from render. ' +
737
'Or maybe you meant to call this function rather than return it.\n' +
738
+ ' <div>{Foo}</div>\n' +
739
' in div (at **)\n' +
740
' in Foo (at **)',
741
'Warning: Functions are not valid as a React child. This may happen if ' +
738
- 'you return a Component instead of <Component /> from render. ' +
742
+ 'you return Foo instead of <Foo /> from render. ' +
743
'Or maybe you meant to call this function rather than return it.\n' +
744
+ ' <span>{Foo}</span>\n' +
745
' in span (at **)\n' +
746
' in div (at **)\n' +
747
' in Foo (at **)',
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+17
-2
@@ -487,8 +487,23 @@ describe('ReactDOMRoot', () => {
487
});
488
}).toErrorDev(
489
'Functions are not valid as a React child. ' +
490
- 'This may happen if you return a Component instead of <Component /> from render. ' +
491
- 'Or maybe you meant to call this function rather than return it.',
490
+ 'This may happen if you return Component instead of <Component /> from render. ' +
491
+ 'Or maybe you meant to call this function rather than return it.\n' +
492
+ ' root.render(Component)',
493
+ {withoutStack: true},
494
+ );
495
+ });
496
+
497
+ it('warns when given a symbol', () => {
498
+ const root = ReactDOMClient.createRoot(document.createElement('div'));
499
+
500
+ expect(() => {
501
+ ReactDOM.flushSync(() => {
502
+ root.render(Symbol('foo'));
503
+ });
504
+ }).toErrorDev(
505
+ 'Symbols are not valid as a React child.\n' +
506
+ ' root.render(Symbol(foo))',
507
{withoutStack: true},
508
);
509
});
packages/react-dom/src/__tests__/ReactLegacyMount-test.js
+3
-2
@@ -71,8 +71,9 @@ describe('ReactMount', () => {
71
72
expect(() => ReactTestUtils.renderIntoDocument(Component)).toErrorDev(
73
'Functions are not valid as a React child. ' +
74
- 'This may happen if you return a Component instead of <Component /> from render. ' +
75
- 'Or maybe you meant to call this function rather than return it.',
74
+ 'This may happen if you return Component instead of <Component /> from render. ' +
75
+ 'Or maybe you meant to call this function rather than return it.\n' +
76
+ ' root.render(Component)',
77
{withoutStack: true},
78
);
79
});
packages/react-reconciler/src/ReactChildFiber.js
+82
-14
@@ -33,7 +33,13 @@ import {
33
REACT_LAZY_TYPE,
34
REACT_CONTEXT_TYPE,
35
} from 'shared/ReactSymbols';
36
-import {ClassComponent, HostText, HostPortal, Fragment} from './ReactWorkTags';
36
+import {
37
+ ClassComponent,
38
+ HostRoot,
39
+ HostText,
40
+ HostPortal,
41
+ Fragment,
42
+} from './ReactWorkTags';
43
import isArray from 'shared/isArray';
44
import {checkPropStringCoercion} from 'shared/CheckStringCoercion';
45
@@ -79,6 +85,7 @@ let didWarnAboutGenerators;
85
let didWarnAboutStringRefs;
86
let ownerHasKeyUseWarning;
87
let ownerHasFunctionTypeWarning;
88
+let ownerHasSymbolTypeWarning;
89
let warnForMissingKey = (child: mixed, returnFiber: Fiber) => {};
90
91
if (__DEV__) {
@@ -93,6 +100,7 @@ if (__DEV__) {
100
*/
101
ownerHasKeyUseWarning = ({}: {[string]: boolean});
102
ownerHasFunctionTypeWarning = ({}: {[string]: boolean});
103
+ ownerHasSymbolTypeWarning = ({}: {[string]: boolean});
104
105
warnForMissingKey = (child: mixed, returnFiber: Fiber) => {
106
if (child === null || typeof child !== 'object') {
@@ -267,20 +275,68 @@ function throwOnInvalidObjectType(returnFiber: Fiber, newChild: Object) {
275
);
276
}
277
270
-function warnOnFunctionType(returnFiber: Fiber) {
278
+function warnOnFunctionType(returnFiber: Fiber, invalidChild: Function) {
279
if (__DEV__) {
272
- const componentName = getComponentNameFromFiber(returnFiber) || 'Component';
280
+ const parentName = getComponentNameFromFiber(returnFiber) || 'Component';
281
274
- if (ownerHasFunctionTypeWarning[componentName]) {
282
+ if (ownerHasFunctionTypeWarning[parentName]) {
283
return;
284
}
277
- ownerHasFunctionTypeWarning[componentName] = true;
285
+ ownerHasFunctionTypeWarning[parentName] = true;
286
+
287
+ const name = invalidChild.displayName || invalidChild.name || 'Component';
288
+
289
+ if (returnFiber.tag === HostRoot) {
290
+ console.error(
291
+ 'Functions are not valid as a React child. This may happen if ' +
292
+ 'you return %s instead of <%s /> from render. ' +
293
+ 'Or maybe you meant to call this function rather than return it.\n' +
294
+ ' root.render(%s)',
295
+ name,
296
+ name,
297
+ name,
298
+ );
299
+ } else {
300
+ console.error(
301
+ 'Functions are not valid as a React child. This may happen if ' +
302
+ 'you return %s instead of <%s /> from render. ' +
303
+ 'Or maybe you meant to call this function rather than return it.\n' +
304
+ ' <%s>{%s}</%s>',
305
+ name,
306
+ name,
307
+ parentName,
308
+ name,
309
+ parentName,
310
+ );
311
+ }
312
+ }
313
+}
314
279
- console.error(
280
- 'Functions are not valid as a React child. This may happen if ' +
281
- 'you return a Component instead of <Component /> from render. ' +
282
- 'Or maybe you meant to call this function rather than return it.',
283
- );
315
+function warnOnSymbolType(returnFiber: Fiber, invalidChild: symbol) {
316
+ if (__DEV__) {
317
+ const parentName = getComponentNameFromFiber(returnFiber) || 'Component';
318
+
319
+ if (ownerHasSymbolTypeWarning[parentName]) {
320
+ return;
321
+ }
322
+ ownerHasSymbolTypeWarning[parentName] = true;
323
+
324
+ // eslint-disable-next-line react-internal/safe-string-coercion
325
+ const name = String(invalidChild);
326
+
327
+ if (returnFiber.tag === HostRoot) {
328
+ console.error(
329
+ 'Symbols are not valid as a React child.\n' + ' root.render(%s)',
330
+ name,
331
+ );
332
+ } else {
333
+ console.error(
334
+ 'Symbols are not valid as a React child.\n' + ' <%s>%s</%s>',
335
+ parentName,
336
+ name,
337
+ parentName,
338
+ );
339
+ }
340
}
341
}
342
@@ -656,7 +712,10 @@ function createChildReconciler(
712
713
if (__DEV__) {
714
if (typeof newChild === 'function') {
659
- warnOnFunctionType(returnFiber);
715
+ warnOnFunctionType(returnFiber, newChild);
716
+ }
717
+ if (typeof newChild === 'symbol') {
718
+ warnOnSymbolType(returnFiber, newChild);
719
}
720
}
721
@@ -778,7 +837,10 @@ function createChildReconciler(
837
838
if (__DEV__) {
839
if (typeof newChild === 'function') {
781
- warnOnFunctionType(returnFiber);
840
+ warnOnFunctionType(returnFiber, newChild);
841
+ }
842
+ if (typeof newChild === 'symbol') {
843
+ warnOnSymbolType(returnFiber, newChild);
844
}
845
}
846
@@ -894,7 +956,10 @@ function createChildReconciler(
956
957
if (__DEV__) {
958
if (typeof newChild === 'function') {
897
- warnOnFunctionType(returnFiber);
959
+ warnOnFunctionType(returnFiber, newChild);
960
+ }
961
+ if (typeof newChild === 'symbol') {
962
+ warnOnSymbolType(returnFiber, newChild);
963
}
964
}
965
@@ -1621,7 +1686,10 @@ function createChildReconciler(
1686
1687
if (__DEV__) {
1688
if (typeof newChild === 'function') {
1624
- warnOnFunctionType(returnFiber);
1689
+ warnOnFunctionType(returnFiber, newChild);
1690
+ }
1691
+ if (typeof newChild === 'symbol') {
1692
+ warnOnSymbolType(returnFiber, newChild);
1693
}
1694
}
1695
packages/react-server/src/ReactFizzServer.js
+25
-5
@@ -2137,6 +2137,27 @@ function validateIterable(iterable, iteratorFn: Function): void {
2137
}
2138
}
2139
2140
+function warnOnFunctionType(invalidChild: Function) {
2141
+ if (__DEV__) {
2142
+ const name = invalidChild.displayName || invalidChild.name || 'Component';
2143
+ console.error(
2144
+ 'Functions are not valid as a React child. This may happen if ' +
2145
+ 'you return %s instead of <%s /> from render. ' +
2146
+ 'Or maybe you meant to call this function rather than return it.',
2147
+ name,
2148
+ name,
2149
+ );
2150
+ }
2151
+}
2152
+
2153
+function warnOnSymbolType(invalidChild: symbol) {
2154
+ if (__DEV__) {
2155
+ // eslint-disable-next-line react-internal/safe-string-coercion
2156
+ const name = String(invalidChild);
2157
+ console.error('Symbols are not valid as a React child.\n' + ' %s', name);
2158
+ }
2159
+}
2160
+
2161
// This function by it self renders a node and consumes the task by mutating it
2162
// to update the current execution state.
2163
function renderNodeDestructive(
@@ -2329,11 +2350,10 @@ function renderNodeDestructive(
2350
2351
if (__DEV__) {
2352
if (typeof node === 'function') {
2332
- console.error(
2333
- 'Functions are not valid as a React child. This may happen if ' +
2334
- 'you return a Component instead of <Component /> from render. ' +
2335
- 'Or maybe you meant to call this function rather than return it.',
2336
- );
2353
+ warnOnFunctionType(node);
2354
+ }
2355
+ if (typeof node === 'symbol') {
2356
+ warnOnSymbolType(node);
2357
}
2358
}
2359
}