@samitouri / QOS-React-2 / commits / e02baf6c92

Warn for invalid type in renderer with the correct RSC stack (#30102)

This is all behind the `enableOwnerStacks` flag. This is a follow up to #29088. In that I moved type validation into the renderer since that's the one that knows what types are allowed. However, I only removed it from `React.createElement` and not the JSX which was an oversight. However, I also noticed that for invalid types we don't have the right stack trace for throws because we're not yet inside the JSX element that itself is invalid. We should use its stack for the stack trace. That's the reason it's enough to just use the throw now because we can get a good stack trace from the owner stack. This is fixed by creating a fake Throw Fiber that gets assigned the right stack. Additionally, I noticed that for certain invalid types like the most common one `undefined` we error in Flight so a missing import in RSC leads to a generic error. Instead of erroring on the Flight side we should just let anything that's not a Server Component through to the client and then let the Client renderer determine whether it's a valid type or not. Since we now have owner stacks through the server too, this will still be able to provide a good stack trace on the client that points to the server in that case. <img width="571" alt="Screenshot 2024-06-25 at 6 46 35 PM" src="https://github.com/facebook/react/assets/63648/6812c24f-e274-4e09-b4de-21deda9ea1d4"> To get the best stack you have to expand the little icon and the regular stack is noisy [due to this Chrome bug](https://issues.chromium.org/issues/345248263) which makes it a little harder to find but once that's fixed it might be easier.

Sebastian Markbåge committed Jun 27, 2024 at 18:10 UTC e02baf6c92833a0d45a77fb2e741676f393c24f7
12 files changed +250 -183
packages/react-client/src/__tests__/ReactFlight-test.js
+15 -7
@@ -692,14 +692,22 @@ describe('ReactFlight', () => {
692
693 const transport = ReactNoopFlightServer.render(<ServerComponent />);
694
695 - await act(async () => {
696 - const rootModel = await ReactNoopFlightClient.read(transport);
697 - ReactNoop.render(rootModel);
698 - });
699 - expect(ReactNoop).toMatchRenderedOutput('Loading...');
700 - spyOnDevAndProd(console, 'error').mockImplementation(() => {});
695 await load();
702 - expect(console.error).toHaveBeenCalledTimes(1);
696 +
697 + await expect(async () => {
698 + await act(async () => {
699 + const rootModel = await ReactNoopFlightClient.read(transport);
700 + ReactNoop.render(rootModel);
701 + });
702 + }).rejects.toThrow(
703 + __DEV__
704 + ? 'Element type is invalid: expected a string (for built-in components) or a class/function ' +
705 + '(for composite components) but got: <div />. ' +
706 + 'Did you accidentally export a JSX literal instead of a component?'
707 + : 'Element type is invalid: expected a string (for built-in components) or a class/function ' +
708 + '(for composite components) but got: object.',
709 + );
710 + expect(ReactNoop).toMatchRenderedOutput(null);
711 });
712
713 it('can render a lazy element', async () => {
packages/react-dom/src/__tests__/ReactComponent-test.js
+58 -26
@@ -14,6 +14,7 @@ let ReactDOM;
14 let ReactDOMClient;
15 let ReactDOMServer;
16 let act;
17 +let assertConsoleErrorDev;
18
19 describe('ReactComponent', () => {
20 beforeEach(() => {
@@ -24,6 +25,8 @@ describe('ReactComponent', () => {
25 ReactDOMClient = require('react-dom/client');
26 ReactDOMServer = require('react-dom/server');
27 act = require('internal-test-utils').act;
28 + assertConsoleErrorDev =
29 + require('internal-test-utils').assertConsoleErrorDev;
30 });
31
32 // @gate !disableLegacyMode
@@ -131,8 +134,6 @@ describe('ReactComponent', () => {
134
135 // @gate !disableStringRefs
136 it('string refs do not detach and reattach on every render', async () => {
134 - spyOnDev(console, 'error').mockImplementation(() => {});
135 -
137 let refVal;
138 class Child extends React.Component {
139 componentDidUpdate() {
@@ -171,6 +172,8 @@ describe('ReactComponent', () => {
172 root.render(<Parent />);
173 });
174
175 + assertConsoleErrorDev(['contains the string ref']);
176 +
177 expect(refVal).toBe(undefined);
178 await act(() => {
179 root.render(<Parent showChild={true} />);
@@ -511,19 +514,25 @@ describe('ReactComponent', () => {
514 });
515
516 it('throws usefully when rendering badly-typed elements', async () => {
517 + const container = document.createElement('div');
518 + const root = ReactDOMClient.createRoot(container);
519 +
520 const X = undefined;
515 - let container = document.createElement('div');
516 - let root = ReactDOMClient.createRoot(container);
517 - await expect(
518 - expect(async () => {
519 - await act(() => {
520 - root.render(<X />);
521 - });
522 - }).toErrorDev(
523 - 'React.jsx: type is invalid -- expected a string (for built-in components) ' +
524 - 'or a class/function (for composite components) but got: undefined.',
525 - ),
526 - ).rejects.toThrowError(
521 + const XElement = <X />;
522 + if (gate(flags => !flags.enableOwnerStacks)) {
523 + assertConsoleErrorDev(
524 + [
525 + 'React.jsx: type is invalid -- expected a string (for built-in components) ' +
526 + 'or a class/function (for composite components) but got: undefined.',
527 + ],
528 + {withoutStack: true},
529 + );
530 + }
531 + await expect(async () => {
532 + await act(() => {
533 + root.render(XElement);
534 + });
535 + }).rejects.toThrowError(
536 'Element type is invalid: expected a string (for built-in components) ' +
537 'or a class/function (for composite components) but got: undefined.' +
538 (__DEV__
@@ -533,21 +542,44 @@ describe('ReactComponent', () => {
542 );
543
544 const Y = null;
536 - container = document.createElement('div');
537 - root = ReactDOMClient.createRoot(container);
538 - await expect(
539 - expect(async () => {
540 - await act(() => {
541 - root.render(<Y />);
542 - });
543 - }).toErrorDev(
544 - 'React.jsx: type is invalid -- expected a string (for built-in components) ' +
545 - 'or a class/function (for composite components) but got: null.',
546 - ),
547 - ).rejects.toThrowError(
545 + const YElement = <Y />;
546 + if (gate(flags => !flags.enableOwnerStacks)) {
547 + assertConsoleErrorDev(
548 + [
549 + 'React.jsx: type is invalid -- expected a string (for built-in components) ' +
550 + 'or a class/function (for composite components) but got: null.',
551 + ],
552 + {withoutStack: true},
553 + );
554 + }
555 + await expect(async () => {
556 + await act(() => {
557 + root.render(YElement);
558 + });
559 + }).rejects.toThrowError(
560 'Element type is invalid: expected a string (for built-in components) ' +
561 'or a class/function (for composite components) but got: null.',
562 );
563 +
564 + const Z = true;
565 + const ZElement = <Z />;
566 + if (gate(flags => !flags.enableOwnerStacks)) {
567 + assertConsoleErrorDev(
568 + [
569 + 'React.jsx: type is invalid -- expected a string (for built-in components) ' +
570 + 'or a class/function (for composite components) but got: boolean.',
571 + ],
572 + {withoutStack: true},
573 + );
574 + }
575 + await expect(async () => {
576 + await act(() => {
577 + root.render(ZElement);
578 + });
579 + }).rejects.toThrowError(
580 + 'Element type is invalid: expected a string (for built-in components) ' +
581 + 'or a class/function (for composite components) but got: boolean.',
582 + );
583 });
584
585 it('includes owner name in the error about badly-typed elements', async () => {
packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js
+19 -13
@@ -987,11 +987,13 @@ describe('ReactDOMServerIntegration', () => {
987 expect(() => {
988 EmptyComponent = <EmptyComponent />;
989 }).toErrorDev(
990 - 'React.jsx: type is invalid -- expected a string ' +
991 - '(for built-in components) or a class/function (for composite ' +
992 - 'components) but got: object. You likely forgot to export your ' +
993 - "component from the file it's defined in, or you might have mixed up " +
994 - 'default and named imports.',
990 + gate(flags => flags.enableOwnerStacks)
991 + ? []
992 + : 'React.jsx: type is invalid -- expected a string ' +
993 + '(for built-in components) or a class/function (for composite ' +
994 + 'components) but got: object. You likely forgot to export your ' +
995 + "component from the file it's defined in, or you might have mixed up " +
996 + 'default and named imports.',
997 {withoutStack: true},
998 );
999 await render(EmptyComponent);
@@ -1011,9 +1013,11 @@ describe('ReactDOMServerIntegration', () => {
1013 expect(() => {
1014 NullComponent = <NullComponent />;
1015 }).toErrorDev(
1014 - 'React.jsx: type is invalid -- expected a string ' +
1015 - '(for built-in components) or a class/function (for composite ' +
1016 - 'components) but got: null.',
1016 + gate(flags => flags.enableOwnerStacks)
1017 + ? []
1018 + : 'React.jsx: type is invalid -- expected a string ' +
1019 + '(for built-in components) or a class/function (for composite ' +
1020 + 'components) but got: null.',
1021 {withoutStack: true},
1022 );
1023 await render(NullComponent);
@@ -1029,11 +1033,13 @@ describe('ReactDOMServerIntegration', () => {
1033 expect(() => {
1034 UndefinedComponent = <UndefinedComponent />;
1035 }).toErrorDev(
1032 - 'React.jsx: type is invalid -- expected a string ' +
1033 - '(for built-in components) or a class/function (for composite ' +
1034 - 'components) but got: undefined. You likely forgot to export your ' +
1035 - "component from the file it's defined in, or you might have mixed up " +
1036 - 'default and named imports.',
1036 + gate(flags => flags.enableOwnerStacks)
1037 + ? []
1038 + : 'React.jsx: type is invalid -- expected a string ' +
1039 + '(for built-in components) or a class/function (for composite ' +
1040 + 'components) but got: undefined. You likely forgot to export your ' +
1041 + "component from the file it's defined in, or you might have mixed up " +
1042 + 'default and named imports.',
1043 {withoutStack: true},
1044 );
1045
packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
+33 -24
@@ -13,6 +13,7 @@ let PropTypes;
13 let React;
14 let ReactDOM;
15 let act;
16 +let assertConsoleErrorDev;
17
18 // TODO: Refactor this test once componentDidCatch setState is deprecated.
19 describe('ReactLegacyErrorBoundaries', () => {
@@ -42,6 +43,8 @@ describe('ReactLegacyErrorBoundaries', () => {
43 ReactDOM = require('react-dom');
44 React = require('react');
45 act = require('internal-test-utils').act;
46 + assertConsoleErrorDev =
47 + require('internal-test-utils').assertConsoleErrorDev;
48
49 log = [];
50
@@ -2099,32 +2102,38 @@ describe('ReactLegacyErrorBoundaries', () => {
2102 const Y = undefined;
2103
2104 await expect(async () => {
2102 - await expect(async () => {
2103 - const container = document.createElement('div');
2104 - await act(() => {
2105 - ReactDOM.render(<X />, container);
2106 - });
2107 - }).rejects.toThrow('got: null');
2108 - }).toErrorDev(
2109 - 'React.jsx: type is invalid -- expected a string ' +
2110 - '(for built-in components) or a class/function ' +
2111 - '(for composite components) but got: null.',
2112 - {withoutStack: 1},
2113 - );
2105 + const container = document.createElement('div');
2106 + await act(() => {
2107 + ReactDOM.render(<X />, container);
2108 + });
2109 + }).rejects.toThrow('got: null');
2110 + if (gate(flags => !flags.enableOwnerStacks)) {
2111 + assertConsoleErrorDev(
2112 + [
2113 + 'React.jsx: type is invalid -- expected a string ' +
2114 + '(for built-in components) or a class/function ' +
2115 + '(for composite components) but got: null.',
2116 + ],
2117 + {withoutStack: true},
2118 + );
2119 + }
2120
2121 await expect(async () => {
2116 - await expect(async () => {
2117 - const container = document.createElement('div');
2118 - await act(() => {
2119 - ReactDOM.render(<Y />, container);
2120 - });
2121 - }).rejects.toThrow('got: undefined');
2122 - }).toErrorDev(
2123 - 'React.jsx: type is invalid -- expected a string ' +
2124 - '(for built-in components) or a class/function ' +
2125 - '(for composite components) but got: undefined.',
2126 - {withoutStack: 1},
2127 - );
2122 + const container = document.createElement('div');
2123 + await act(() => {
2124 + ReactDOM.render(<Y />, container);
2125 + });
2126 + }).rejects.toThrow('got: undefined');
2127 + if (gate(flags => !flags.enableOwnerStacks)) {
2128 + assertConsoleErrorDev(
2129 + [
2130 + 'React.jsx: type is invalid -- expected a string ' +
2131 + '(for built-in components) or a class/function ' +
2132 + '(for composite components) but got: undefined.',
2133 + ],
2134 + {withoutStack: true},
2135 + );
2136 + }
2137 });
2138
2139 // @gate !disableLegacyMode
packages/react-reconciler/src/ReactChildFiber.js
+6
@@ -220,6 +220,9 @@ function validateFragmentProps(
220 // For unkeyed root fragments there's no Fiber. We create a fake one just for
221 // error stack handling.
222 fiber = createFiberFromElement(element, returnFiber.mode, 0);
223 + if (__DEV__) {
224 + fiber._debugInfo = currentDebugInfo;
225 + }
226 fiber.return = returnFiber;
227 }
228 runWithFiberInDEV(
@@ -242,6 +245,9 @@ function validateFragmentProps(
245 // For unkeyed root fragments there's no Fiber. We create a fake one just for
246 // error stack handling.
247 fiber = createFiberFromElement(element, returnFiber.mode, 0);
248 + if (__DEV__) {
249 + fiber._debugInfo = currentDebugInfo;
250 + }
251 fiber.return = returnFiber;
252 }
253 runWithFiberInDEV(fiber, () => {
packages/react-reconciler/src/ReactFiber.js
+9 -1
@@ -485,6 +485,7 @@ export function createHostRootFiber(
485 return createFiber(HostRoot, null, null, mode);
486 }
487
488 +// TODO: Get rid of this helper. Only createFiberFromElement should exist.
489 export function createFiberFromTypeAndProps(
490 type: any, // React$ElementType
491 key: null | string,
@@ -650,11 +651,18 @@ export function createFiberFromTypeAndProps(
651 typeString = type === null ? 'null' : typeof type;
652 }
653
653 - throw new Error(
654 + // The type is invalid but it's conceptually a child that errored and not the
655 + // current component itself so we create a virtual child that throws in its
656 + // begin phase. This is the same thing we do in ReactChildFiber if we throw
657 + // but we do it here so that we can assign the debug owner and stack from the
658 + // element itself. That way the error stack will point to the JSX callsite.
659 + fiberTag = Throw;
660 + pendingProps = new Error(
661 'Element type is invalid: expected a string (for built-in ' +
662 'components) or a class/function (for composite components) ' +
663 `but got: ${typeString}.${info}`,
664 );
665 + resolvedType = null;
666 }
667 }
668 }
packages/react-reconciler/src/__tests__/ErrorBoundaryReconciliation-test.internal.js
+14 -9
@@ -6,6 +6,7 @@ describe('ErrorBoundaryReconciliation', () => {
6 let ReactTestRenderer;
7 let span;
8 let act;
9 + let assertConsoleErrorDev;
10
11 beforeEach(() => {
12 jest.resetModules();
@@ -13,6 +14,8 @@ describe('ErrorBoundaryReconciliation', () => {
14 ReactTestRenderer = require('react-test-renderer');
15 React = require('react');
16 act = require('internal-test-utils').act;
17 + assertConsoleErrorDev =
18 + require('internal-test-utils').assertConsoleErrorDev;
19 DidCatchErrorBoundary = class extends React.Component {
20 state = {error: null};
21 componentDidCatch(error) {
@@ -58,15 +61,17 @@ describe('ErrorBoundaryReconciliation', () => {
61 );
62 });
63 expect(renderer).toMatchRenderedOutput(<span prop="BrokenRender" />);
61 - await expect(async () => {
62 - await act(() => {
63 - renderer.update(
64 - <ErrorBoundary fallbackTagName={fallbackTagName}>
65 - <BrokenRender fail={true} />
66 - </ErrorBoundary>,
67 - );
68 - });
69 - }).toErrorDev(['invalid', 'invalid']);
64 + await act(() => {
65 + renderer.update(
66 + <ErrorBoundary fallbackTagName={fallbackTagName}>
67 + <BrokenRender fail={true} />
68 + </ErrorBoundary>,
69 + );
70 + });
71 + if (gate(flags => !flags.enableOwnerStacks)) {
72 + assertConsoleErrorDev(['invalid', 'invalid']);
73 + }
74 +
75 const Fallback = fallbackTagName;
76 expect(renderer).toMatchRenderedOutput(<Fallback prop="ErrorBoundary" />);
77 }
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+28 -14
@@ -19,6 +19,7 @@ let assertLog;
19 let waitForAll;
20 let waitFor;
21 let waitForThrow;
22 +let assertConsoleErrorDev;
23
24 describe('ReactIncrementalErrorHandling', () => {
25 beforeEach(() => {
@@ -28,6 +29,8 @@ describe('ReactIncrementalErrorHandling', () => {
29 ReactNoop = require('react-noop-renderer');
30 Scheduler = require('scheduler');
31 act = require('internal-test-utils').act;
32 + assertConsoleErrorDev =
33 + require('internal-test-utils').assertConsoleErrorDev;
34
35 const InternalTestUtils = require('internal-test-utils');
36 assertLog = InternalTestUtils.assertLog;
@@ -1237,11 +1240,15 @@ describe('ReactIncrementalErrorHandling', () => {
1240 <BrokenRender />
1241 </ErrorBoundary>,
1242 );
1240 - await expect(async () => await waitForAll([])).toErrorDev([
1241 - 'React.jsx: type is invalid -- expected a string',
1242 - // React retries once on error
1243 - 'React.jsx: type is invalid -- expected a string',
1244 - ]);
1243 + await waitForAll([]);
1244 + if (gate(flags => !flags.enableOwnerStacks)) {
1245 + assertConsoleErrorDev([
1246 + 'React.jsx: type is invalid -- expected a string',
1247 + // React retries once on error
1248 + 'React.jsx: type is invalid -- expected a string',
1249 + ]);
1250 + }
1251 +
1252 expect(ReactNoop).toMatchRenderedOutput(
1253 <span
1254 prop={
@@ -1288,11 +1295,14 @@ describe('ReactIncrementalErrorHandling', () => {
1295 <BrokenRender fail={true} />
1296 </ErrorBoundary>,
1297 );
1291 - await expect(async () => await waitForAll([])).toErrorDev([
1292 - 'React.jsx: type is invalid -- expected a string',
1293 - // React retries once on error
1294 - 'React.jsx: type is invalid -- expected a string',
1295 - ]);
1298 + await waitForAll([]);
1299 + if (gate(flags => !flags.enableOwnerStacks)) {
1300 + assertConsoleErrorDev([
1301 + 'React.jsx: type is invalid -- expected a string',
1302 + // React retries once on error
1303 + 'React.jsx: type is invalid -- expected a string',
1304 + ]);
1305 + }
1306 expect(ReactNoop).toMatchRenderedOutput(
1307 <span
1308 prop={
@@ -1310,10 +1320,14 @@ describe('ReactIncrementalErrorHandling', () => {
1320
1321 it('recovers from uncaught reconciler errors', async () => {
1322 const InvalidType = undefined;
1313 - expect(() => ReactNoop.render(<InvalidType />)).toErrorDev(
1314 - 'React.jsx: type is invalid -- expected a string',
1315 - {withoutStack: true},
1316 - );
1323 + ReactNoop.render(<InvalidType />);
1324 + if (gate(flags => !flags.enableOwnerStacks)) {
1325 + assertConsoleErrorDev(
1326 + ['React.jsx: type is invalid -- expected a string'],
1327 + {withoutStack: true},
1328 + );
1329 + }
1330 +
1331 await waitForThrow(
1332 'Element type is invalid: expected a string (for built-in components) or ' +
1333 'a class/function (for composite components) but got: undefined.' +
packages/react-server/src/ReactFlightServer.js
+39 -54
@@ -110,7 +110,6 @@ import {
110 } from 'shared/ReactSymbols';
111
112 import {
113 - describeValueForErrorMessage,
113 describeObjectForErrorMessage,
114 isSimpleObject,
115 jsxPropsParents,
@@ -1501,19 +1500,11 @@ function renderElement(
1500 jsxChildrenParents.set(props.children, type);
1501 }
1502 }
1504 - if (typeof type === 'function') {
1505 - if (isClientReference(type) || isOpaqueTemporaryReference(type)) {
1506 - // This is a reference to a Client Component.
1507 - return renderClientElement(
1508 - task,
1509 - type,
1510 - key,
1511 - props,
1512 - owner,
1513 - stack,
1514 - validated,
1515 - );
1516 - }
1503 + if (
1504 + typeof type === 'function' &&
1505 + !isClientReference(type) &&
1506 + !isOpaqueTemporaryReference(type)
1507 + ) {
1508 // This is a Server Component.
1509 return renderFunctionComponent(
1510 request,
@@ -1525,43 +1516,27 @@ function renderElement(
1516 stack,
1517 validated,
1518 );
1528 - } else if (typeof type === 'string') {
1529 - // This is a host element. E.g. HTML.
1530 - return renderClientElement(task, type, key, props, owner, stack, validated);
1531 - } else if (typeof type === 'symbol') {
1532 - if (type === REACT_FRAGMENT_TYPE && key === null) {
1533 - // For key-less fragments, we add a small optimization to avoid serializing
1534 - // it as a wrapper.
1535 - const prevImplicitSlot = task.implicitSlot;
1536 - if (task.keyPath === null) {
1537 - task.implicitSlot = true;
1538 - }
1539 - const json = renderModelDestructive(
1540 - request,
1541 - task,
1542 - emptyRoot,
1543 - '',
1544 - props.children,
1545 - );
1546 - task.implicitSlot = prevImplicitSlot;
1547 - return json;
1548 - }
1549 - // This might be a built-in React component. We'll let the client decide.
1550 - // Any built-in works as long as its props are serializable.
1551 - return renderClientElement(task, type, key, props, owner, stack, validated);
1552 - } else if (type != null && typeof type === 'object') {
1553 - if (isClientReference(type)) {
1554 - // This is a reference to a Client Component.
1555 - return renderClientElement(
1556 - task,
1557 - type,
1558 - key,
1559 - props,
1560 - owner,
1561 - stack,
1562 - validated,
1563 - );
1564 - }
1519 + } else if (type === REACT_FRAGMENT_TYPE && key === null) {
1520 + // For key-less fragments, we add a small optimization to avoid serializing
1521 + // it as a wrapper.
1522 + const prevImplicitSlot = task.implicitSlot;
1523 + if (task.keyPath === null) {
1524 + task.implicitSlot = true;
1525 + }
1526 + const json = renderModelDestructive(
1527 + request,
1528 + task,
1529 + emptyRoot,
1530 + '',
1531 + props.children,
1532 + );
1533 + task.implicitSlot = prevImplicitSlot;
1534 + return json;
1535 + } else if (
1536 + type != null &&
1537 + typeof type === 'object' &&
1538 + !isClientReference(type)
1539 + ) {
1540 switch (type.$$typeof) {
1541 case REACT_LAZY_TYPE: {
1542 let wrappedType;
@@ -1615,11 +1590,21 @@ function renderElement(
1590 validated,
1591 );
1592 }
1593 + case REACT_ELEMENT_TYPE: {
1594 + // This is invalid but we'll let the client determine that it is.
1595 + if (__DEV__) {
1596 + // Disable the key warning that would happen otherwise because this
1597 + // element gets serialized inside an array. We'll error later anyway.
1598 + type._store.validated = 1;
1599 + }
1600 + }
1601 }
1602 }
1620 - throw new Error(
1621 - `Unsupported Server Component type: ${describeValueForErrorMessage(type)}`,
1622 - );
1603 + // For anything else, try it on the client instead.
1604 + // We don't know if the client will support it or not. This might error on the
1605 + // client or error during serialization but the stack will point back to the
1606 + // server.
1607 + return renderClientElement(task, type, key, props, owner, stack, validated);
1608 }
1609
1610 function pingTask(request: Request, task: Task): void {
packages/react/src/__tests__/ReactElementValidator-test.internal.js
+9 -5
@@ -515,11 +515,15 @@ describe('ReactElementValidator', () => {
515 expect(() => {
516 void (<Foo>{[<div />]}</Foo>);
517 }).toErrorDev(
518 - 'React.jsx: type is invalid -- expected a string ' +
519 - '(for built-in components) or a class/function (for composite ' +
520 - 'components) but got: undefined. You likely forgot to export your ' +
521 - "component from the file it's defined in, or you might have mixed up " +
522 - 'default and named imports.',
518 + gate(flags => flags.enableOwnerStacks)
519 + ? []
520 + : [
521 + 'React.jsx: type is invalid -- expected a string ' +
522 + '(for built-in components) or a class/function (for composite ' +
523 + 'components) but got: undefined. You likely forgot to export your ' +
524 + "component from the file it's defined in, or you might have mixed up " +
525 + 'default and named imports.',
526 + ],
527 {withoutStack: true},
528 );
529 });
packages/react/src/__tests__/ReactJSXElementValidator-test.js
-29
@@ -215,35 +215,6 @@ describe('ReactJSXElementValidator', () => {
215 );
216 });
217
218 - it('gives a helpful error when passing null, undefined, or boolean', () => {
219 - const Undefined = undefined;
220 - const Null = null;
221 - const True = true;
222 - const Div = 'div';
223 - expect(() => void (<Undefined />)).toErrorDev(
224 - 'React.jsx: type is invalid -- expected a string ' +
225 - '(for built-in components) or a class/function (for composite ' +
226 - 'components) but got: undefined. You likely forgot to export your ' +
227 - "component from the file it's defined in, or you might have mixed up " +
228 - 'default and named imports.',
229 - {withoutStack: true},
230 - );
231 - expect(() => void (<Null />)).toErrorDev(
232 - 'React.jsx: type is invalid -- expected a string ' +
233 - '(for built-in components) or a class/function (for composite ' +
234 - 'components) but got: null.',
235 - {withoutStack: true},
236 - );
237 - expect(() => void (<True />)).toErrorDev(
238 - 'React.jsx: type is invalid -- expected a string ' +
239 - '(for built-in components) or a class/function (for composite ' +
240 - 'components) but got: boolean.',
241 - {withoutStack: true},
242 - );
243 - // No error expected
244 - void (<Div />);
245 - });
246 -
218 it('warns for fragments with illegal attributes', async () => {
219 class Foo extends React.Component {
220 render() {
packages/react/src/jsx/ReactJSXElement.js
+20 -1
@@ -559,9 +559,14 @@ function jsxDEVImpl(
559 debugTask,
560 ) {
561 if (__DEV__) {
562 - if (!isValidElementType(type)) {
562 + if (!enableOwnerStacks && !isValidElementType(type)) {
563 // This is an invalid element type.
564 //
565 + // We warn here so that we can get better stack traces but with enableOwnerStacks
566 + // enabled we don't need this because we get good stacks if we error in the
567 + // renderer anyway. The renderer is the only one that knows what types are valid
568 + // for this particular renderer so we let it error there instead.
569 + //
570 // We warn in this case but don't throw. We expect the element creation to
571 // succeed and there will likely be errors in render.
572 let info = '';
@@ -604,6 +609,9 @@ function jsxDEVImpl(
609 // errors. We don't want exception behavior to differ between dev and
610 // prod. (Rendering will throw with a helpful message and as soon as the
611 // type is fixed, the key warnings will appear.)
612 + // When enableOwnerStacks is on, we no longer need the type here so this
613 + // comment is no longer true. Which is why we can run this even for invalid
614 + // types.
615 const children = config.children;
616 if (children !== undefined) {
617 if (isStaticChildren) {
@@ -1103,6 +1111,17 @@ export function cloneElement(element, config, children) {
1111 */
1112 function validateChildKeys(node, parentType) {
1113 if (__DEV__) {
1114 + if (enableOwnerStacks) {
1115 + // When owner stacks is enabled no warnings happens. All we do is
1116 + // mark elements as being in a valid static child position so they
1117 + // don't need keys.
1118 + if (isValidElement(node)) {
1119 + if (node._store) {
1120 + node._store.validated = 1;
1121 + }
1122 + }
1123 + return;
1124 + }
1125 if (typeof node !== 'object' || !node) {
1126 return;
1127 }