mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
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.
This commit is contained in:
parent
ffec9ec5b5
commit
e02baf6c92
|
|
@ -692,14 +692,22 @@ describe('ReactFlight', () => {
|
||||||
|
|
||||||
const transport = ReactNoopFlightServer.render(<ServerComponent />);
|
const transport = ReactNoopFlightServer.render(<ServerComponent />);
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
const rootModel = await ReactNoopFlightClient.read(transport);
|
|
||||||
ReactNoop.render(rootModel);
|
|
||||||
});
|
|
||||||
expect(ReactNoop).toMatchRenderedOutput('Loading...');
|
|
||||||
spyOnDevAndProd(console, 'error').mockImplementation(() => {});
|
|
||||||
await load();
|
await load();
|
||||||
expect(console.error).toHaveBeenCalledTimes(1);
|
|
||||||
|
await expect(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
const rootModel = await ReactNoopFlightClient.read(transport);
|
||||||
|
ReactNoop.render(rootModel);
|
||||||
|
});
|
||||||
|
}).rejects.toThrow(
|
||||||
|
__DEV__
|
||||||
|
? 'Element type is invalid: expected a string (for built-in components) or a class/function ' +
|
||||||
|
'(for composite components) but got: <div />. ' +
|
||||||
|
'Did you accidentally export a JSX literal instead of a component?'
|
||||||
|
: 'Element type is invalid: expected a string (for built-in components) or a class/function ' +
|
||||||
|
'(for composite components) but got: object.',
|
||||||
|
);
|
||||||
|
expect(ReactNoop).toMatchRenderedOutput(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can render a lazy element', async () => {
|
it('can render a lazy element', async () => {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ let ReactDOM;
|
||||||
let ReactDOMClient;
|
let ReactDOMClient;
|
||||||
let ReactDOMServer;
|
let ReactDOMServer;
|
||||||
let act;
|
let act;
|
||||||
|
let assertConsoleErrorDev;
|
||||||
|
|
||||||
describe('ReactComponent', () => {
|
describe('ReactComponent', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -24,6 +25,8 @@ describe('ReactComponent', () => {
|
||||||
ReactDOMClient = require('react-dom/client');
|
ReactDOMClient = require('react-dom/client');
|
||||||
ReactDOMServer = require('react-dom/server');
|
ReactDOMServer = require('react-dom/server');
|
||||||
act = require('internal-test-utils').act;
|
act = require('internal-test-utils').act;
|
||||||
|
assertConsoleErrorDev =
|
||||||
|
require('internal-test-utils').assertConsoleErrorDev;
|
||||||
});
|
});
|
||||||
|
|
||||||
// @gate !disableLegacyMode
|
// @gate !disableLegacyMode
|
||||||
|
|
@ -131,8 +134,6 @@ describe('ReactComponent', () => {
|
||||||
|
|
||||||
// @gate !disableStringRefs
|
// @gate !disableStringRefs
|
||||||
it('string refs do not detach and reattach on every render', async () => {
|
it('string refs do not detach and reattach on every render', async () => {
|
||||||
spyOnDev(console, 'error').mockImplementation(() => {});
|
|
||||||
|
|
||||||
let refVal;
|
let refVal;
|
||||||
class Child extends React.Component {
|
class Child extends React.Component {
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
|
|
@ -171,6 +172,8 @@ describe('ReactComponent', () => {
|
||||||
root.render(<Parent />);
|
root.render(<Parent />);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assertConsoleErrorDev(['contains the string ref']);
|
||||||
|
|
||||||
expect(refVal).toBe(undefined);
|
expect(refVal).toBe(undefined);
|
||||||
await act(() => {
|
await act(() => {
|
||||||
root.render(<Parent showChild={true} />);
|
root.render(<Parent showChild={true} />);
|
||||||
|
|
@ -511,19 +514,25 @@ describe('ReactComponent', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws usefully when rendering badly-typed elements', async () => {
|
it('throws usefully when rendering badly-typed elements', async () => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const root = ReactDOMClient.createRoot(container);
|
||||||
|
|
||||||
const X = undefined;
|
const X = undefined;
|
||||||
let container = document.createElement('div');
|
const XElement = <X />;
|
||||||
let root = ReactDOMClient.createRoot(container);
|
if (gate(flags => !flags.enableOwnerStacks)) {
|
||||||
await expect(
|
assertConsoleErrorDev(
|
||||||
expect(async () => {
|
[
|
||||||
await act(() => {
|
'React.jsx: type is invalid -- expected a string (for built-in components) ' +
|
||||||
root.render(<X />);
|
'or a class/function (for composite components) but got: undefined.',
|
||||||
});
|
],
|
||||||
}).toErrorDev(
|
{withoutStack: true},
|
||||||
'React.jsx: type is invalid -- expected a string (for built-in components) ' +
|
);
|
||||||
'or a class/function (for composite components) but got: undefined.',
|
}
|
||||||
),
|
await expect(async () => {
|
||||||
).rejects.toThrowError(
|
await act(() => {
|
||||||
|
root.render(XElement);
|
||||||
|
});
|
||||||
|
}).rejects.toThrowError(
|
||||||
'Element type is invalid: expected a string (for built-in components) ' +
|
'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.' +
|
||||||
(__DEV__
|
(__DEV__
|
||||||
|
|
@ -533,21 +542,44 @@ describe('ReactComponent', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const Y = null;
|
const Y = null;
|
||||||
container = document.createElement('div');
|
const YElement = <Y />;
|
||||||
root = ReactDOMClient.createRoot(container);
|
if (gate(flags => !flags.enableOwnerStacks)) {
|
||||||
await expect(
|
assertConsoleErrorDev(
|
||||||
expect(async () => {
|
[
|
||||||
await act(() => {
|
'React.jsx: type is invalid -- expected a string (for built-in components) ' +
|
||||||
root.render(<Y />);
|
'or a class/function (for composite components) but got: null.',
|
||||||
});
|
],
|
||||||
}).toErrorDev(
|
{withoutStack: true},
|
||||||
'React.jsx: type is invalid -- expected a string (for built-in components) ' +
|
);
|
||||||
'or a class/function (for composite components) but got: null.',
|
}
|
||||||
),
|
await expect(async () => {
|
||||||
).rejects.toThrowError(
|
await act(() => {
|
||||||
|
root.render(YElement);
|
||||||
|
});
|
||||||
|
}).rejects.toThrowError(
|
||||||
'Element type is invalid: expected a string (for built-in components) ' +
|
'Element type is invalid: expected a string (for built-in components) ' +
|
||||||
'or a class/function (for composite components) but got: null.',
|
'or a class/function (for composite components) but got: null.',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const Z = true;
|
||||||
|
const ZElement = <Z />;
|
||||||
|
if (gate(flags => !flags.enableOwnerStacks)) {
|
||||||
|
assertConsoleErrorDev(
|
||||||
|
[
|
||||||
|
'React.jsx: type is invalid -- expected a string (for built-in components) ' +
|
||||||
|
'or a class/function (for composite components) but got: boolean.',
|
||||||
|
],
|
||||||
|
{withoutStack: true},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await expect(async () => {
|
||||||
|
await act(() => {
|
||||||
|
root.render(ZElement);
|
||||||
|
});
|
||||||
|
}).rejects.toThrowError(
|
||||||
|
'Element type is invalid: expected a string (for built-in components) ' +
|
||||||
|
'or a class/function (for composite components) but got: boolean.',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('includes owner name in the error about badly-typed elements', async () => {
|
it('includes owner name in the error about badly-typed elements', async () => {
|
||||||
|
|
|
||||||
|
|
@ -987,11 +987,13 @@ describe('ReactDOMServerIntegration', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
EmptyComponent = <EmptyComponent />;
|
EmptyComponent = <EmptyComponent />;
|
||||||
}).toErrorDev(
|
}).toErrorDev(
|
||||||
'React.jsx: type is invalid -- expected a string ' +
|
gate(flags => flags.enableOwnerStacks)
|
||||||
'(for built-in components) or a class/function (for composite ' +
|
? []
|
||||||
'components) but got: object. You likely forgot to export your ' +
|
: 'React.jsx: type is invalid -- expected a string ' +
|
||||||
"component from the file it's defined in, or you might have mixed up " +
|
'(for built-in components) or a class/function (for composite ' +
|
||||||
'default and named imports.',
|
'components) but got: object. You likely forgot to export your ' +
|
||||||
|
"component from the file it's defined in, or you might have mixed up " +
|
||||||
|
'default and named imports.',
|
||||||
{withoutStack: true},
|
{withoutStack: true},
|
||||||
);
|
);
|
||||||
await render(EmptyComponent);
|
await render(EmptyComponent);
|
||||||
|
|
@ -1011,9 +1013,11 @@ describe('ReactDOMServerIntegration', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
NullComponent = <NullComponent />;
|
NullComponent = <NullComponent />;
|
||||||
}).toErrorDev(
|
}).toErrorDev(
|
||||||
'React.jsx: type is invalid -- expected a string ' +
|
gate(flags => flags.enableOwnerStacks)
|
||||||
'(for built-in components) or a class/function (for composite ' +
|
? []
|
||||||
'components) but got: null.',
|
: 'React.jsx: type is invalid -- expected a string ' +
|
||||||
|
'(for built-in components) or a class/function (for composite ' +
|
||||||
|
'components) but got: null.',
|
||||||
{withoutStack: true},
|
{withoutStack: true},
|
||||||
);
|
);
|
||||||
await render(NullComponent);
|
await render(NullComponent);
|
||||||
|
|
@ -1029,11 +1033,13 @@ describe('ReactDOMServerIntegration', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
UndefinedComponent = <UndefinedComponent />;
|
UndefinedComponent = <UndefinedComponent />;
|
||||||
}).toErrorDev(
|
}).toErrorDev(
|
||||||
'React.jsx: type is invalid -- expected a string ' +
|
gate(flags => flags.enableOwnerStacks)
|
||||||
'(for built-in components) or a class/function (for composite ' +
|
? []
|
||||||
'components) but got: undefined. You likely forgot to export your ' +
|
: 'React.jsx: type is invalid -- expected a string ' +
|
||||||
"component from the file it's defined in, or you might have mixed up " +
|
'(for built-in components) or a class/function (for composite ' +
|
||||||
'default and named imports.',
|
'components) but got: undefined. You likely forgot to export your ' +
|
||||||
|
"component from the file it's defined in, or you might have mixed up " +
|
||||||
|
'default and named imports.',
|
||||||
{withoutStack: true},
|
{withoutStack: true},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ let PropTypes;
|
||||||
let React;
|
let React;
|
||||||
let ReactDOM;
|
let ReactDOM;
|
||||||
let act;
|
let act;
|
||||||
|
let assertConsoleErrorDev;
|
||||||
|
|
||||||
// TODO: Refactor this test once componentDidCatch setState is deprecated.
|
// TODO: Refactor this test once componentDidCatch setState is deprecated.
|
||||||
describe('ReactLegacyErrorBoundaries', () => {
|
describe('ReactLegacyErrorBoundaries', () => {
|
||||||
|
|
@ -42,6 +43,8 @@ describe('ReactLegacyErrorBoundaries', () => {
|
||||||
ReactDOM = require('react-dom');
|
ReactDOM = require('react-dom');
|
||||||
React = require('react');
|
React = require('react');
|
||||||
act = require('internal-test-utils').act;
|
act = require('internal-test-utils').act;
|
||||||
|
assertConsoleErrorDev =
|
||||||
|
require('internal-test-utils').assertConsoleErrorDev;
|
||||||
|
|
||||||
log = [];
|
log = [];
|
||||||
|
|
||||||
|
|
@ -2099,32 +2102,38 @@ describe('ReactLegacyErrorBoundaries', () => {
|
||||||
const Y = undefined;
|
const Y = undefined;
|
||||||
|
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await expect(async () => {
|
const container = document.createElement('div');
|
||||||
const container = document.createElement('div');
|
await act(() => {
|
||||||
await act(() => {
|
ReactDOM.render(<X />, container);
|
||||||
ReactDOM.render(<X />, container);
|
});
|
||||||
});
|
}).rejects.toThrow('got: null');
|
||||||
}).rejects.toThrow('got: null');
|
if (gate(flags => !flags.enableOwnerStacks)) {
|
||||||
}).toErrorDev(
|
assertConsoleErrorDev(
|
||||||
'React.jsx: type is invalid -- expected a string ' +
|
[
|
||||||
'(for built-in components) or a class/function ' +
|
'React.jsx: type is invalid -- expected a string ' +
|
||||||
'(for composite components) but got: null.',
|
'(for built-in components) or a class/function ' +
|
||||||
{withoutStack: 1},
|
'(for composite components) but got: null.',
|
||||||
);
|
],
|
||||||
|
{withoutStack: true},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await expect(async () => {
|
const container = document.createElement('div');
|
||||||
const container = document.createElement('div');
|
await act(() => {
|
||||||
await act(() => {
|
ReactDOM.render(<Y />, container);
|
||||||
ReactDOM.render(<Y />, container);
|
});
|
||||||
});
|
}).rejects.toThrow('got: undefined');
|
||||||
}).rejects.toThrow('got: undefined');
|
if (gate(flags => !flags.enableOwnerStacks)) {
|
||||||
}).toErrorDev(
|
assertConsoleErrorDev(
|
||||||
'React.jsx: type is invalid -- expected a string ' +
|
[
|
||||||
'(for built-in components) or a class/function ' +
|
'React.jsx: type is invalid -- expected a string ' +
|
||||||
'(for composite components) but got: undefined.',
|
'(for built-in components) or a class/function ' +
|
||||||
{withoutStack: 1},
|
'(for composite components) but got: undefined.',
|
||||||
);
|
],
|
||||||
|
{withoutStack: true},
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// @gate !disableLegacyMode
|
// @gate !disableLegacyMode
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,9 @@ function validateFragmentProps(
|
||||||
// For unkeyed root fragments there's no Fiber. We create a fake one just for
|
// For unkeyed root fragments there's no Fiber. We create a fake one just for
|
||||||
// error stack handling.
|
// error stack handling.
|
||||||
fiber = createFiberFromElement(element, returnFiber.mode, 0);
|
fiber = createFiberFromElement(element, returnFiber.mode, 0);
|
||||||
|
if (__DEV__) {
|
||||||
|
fiber._debugInfo = currentDebugInfo;
|
||||||
|
}
|
||||||
fiber.return = returnFiber;
|
fiber.return = returnFiber;
|
||||||
}
|
}
|
||||||
runWithFiberInDEV(
|
runWithFiberInDEV(
|
||||||
|
|
@ -242,6 +245,9 @@ function validateFragmentProps(
|
||||||
// For unkeyed root fragments there's no Fiber. We create a fake one just for
|
// For unkeyed root fragments there's no Fiber. We create a fake one just for
|
||||||
// error stack handling.
|
// error stack handling.
|
||||||
fiber = createFiberFromElement(element, returnFiber.mode, 0);
|
fiber = createFiberFromElement(element, returnFiber.mode, 0);
|
||||||
|
if (__DEV__) {
|
||||||
|
fiber._debugInfo = currentDebugInfo;
|
||||||
|
}
|
||||||
fiber.return = returnFiber;
|
fiber.return = returnFiber;
|
||||||
}
|
}
|
||||||
runWithFiberInDEV(fiber, () => {
|
runWithFiberInDEV(fiber, () => {
|
||||||
|
|
|
||||||
10
packages/react-reconciler/src/ReactFiber.js
vendored
10
packages/react-reconciler/src/ReactFiber.js
vendored
|
|
@ -485,6 +485,7 @@ export function createHostRootFiber(
|
||||||
return createFiber(HostRoot, null, null, mode);
|
return createFiber(HostRoot, null, null, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Get rid of this helper. Only createFiberFromElement should exist.
|
||||||
export function createFiberFromTypeAndProps(
|
export function createFiberFromTypeAndProps(
|
||||||
type: any, // React$ElementType
|
type: any, // React$ElementType
|
||||||
key: null | string,
|
key: null | string,
|
||||||
|
|
@ -650,11 +651,18 @@ export function createFiberFromTypeAndProps(
|
||||||
typeString = type === null ? 'null' : typeof type;
|
typeString = type === null ? 'null' : typeof type;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
// The type is invalid but it's conceptually a child that errored and not the
|
||||||
|
// current component itself so we create a virtual child that throws in its
|
||||||
|
// begin phase. This is the same thing we do in ReactChildFiber if we throw
|
||||||
|
// but we do it here so that we can assign the debug owner and stack from the
|
||||||
|
// element itself. That way the error stack will point to the JSX callsite.
|
||||||
|
fiberTag = Throw;
|
||||||
|
pendingProps = new Error(
|
||||||
'Element type is invalid: expected a string (for built-in ' +
|
'Element type is invalid: expected a string (for built-in ' +
|
||||||
'components) or a class/function (for composite components) ' +
|
'components) or a class/function (for composite components) ' +
|
||||||
`but got: ${typeString}.${info}`,
|
`but got: ${typeString}.${info}`,
|
||||||
);
|
);
|
||||||
|
resolvedType = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ describe('ErrorBoundaryReconciliation', () => {
|
||||||
let ReactTestRenderer;
|
let ReactTestRenderer;
|
||||||
let span;
|
let span;
|
||||||
let act;
|
let act;
|
||||||
|
let assertConsoleErrorDev;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
|
|
@ -13,6 +14,8 @@ describe('ErrorBoundaryReconciliation', () => {
|
||||||
ReactTestRenderer = require('react-test-renderer');
|
ReactTestRenderer = require('react-test-renderer');
|
||||||
React = require('react');
|
React = require('react');
|
||||||
act = require('internal-test-utils').act;
|
act = require('internal-test-utils').act;
|
||||||
|
assertConsoleErrorDev =
|
||||||
|
require('internal-test-utils').assertConsoleErrorDev;
|
||||||
DidCatchErrorBoundary = class extends React.Component {
|
DidCatchErrorBoundary = class extends React.Component {
|
||||||
state = {error: null};
|
state = {error: null};
|
||||||
componentDidCatch(error) {
|
componentDidCatch(error) {
|
||||||
|
|
@ -58,15 +61,17 @@ describe('ErrorBoundaryReconciliation', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(renderer).toMatchRenderedOutput(<span prop="BrokenRender" />);
|
expect(renderer).toMatchRenderedOutput(<span prop="BrokenRender" />);
|
||||||
await expect(async () => {
|
await act(() => {
|
||||||
await act(() => {
|
renderer.update(
|
||||||
renderer.update(
|
<ErrorBoundary fallbackTagName={fallbackTagName}>
|
||||||
<ErrorBoundary fallbackTagName={fallbackTagName}>
|
<BrokenRender fail={true} />
|
||||||
<BrokenRender fail={true} />
|
</ErrorBoundary>,
|
||||||
</ErrorBoundary>,
|
);
|
||||||
);
|
});
|
||||||
});
|
if (gate(flags => !flags.enableOwnerStacks)) {
|
||||||
}).toErrorDev(['invalid', 'invalid']);
|
assertConsoleErrorDev(['invalid', 'invalid']);
|
||||||
|
}
|
||||||
|
|
||||||
const Fallback = fallbackTagName;
|
const Fallback = fallbackTagName;
|
||||||
expect(renderer).toMatchRenderedOutput(<Fallback prop="ErrorBoundary" />);
|
expect(renderer).toMatchRenderedOutput(<Fallback prop="ErrorBoundary" />);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ let assertLog;
|
||||||
let waitForAll;
|
let waitForAll;
|
||||||
let waitFor;
|
let waitFor;
|
||||||
let waitForThrow;
|
let waitForThrow;
|
||||||
|
let assertConsoleErrorDev;
|
||||||
|
|
||||||
describe('ReactIncrementalErrorHandling', () => {
|
describe('ReactIncrementalErrorHandling', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -28,6 +29,8 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||||
ReactNoop = require('react-noop-renderer');
|
ReactNoop = require('react-noop-renderer');
|
||||||
Scheduler = require('scheduler');
|
Scheduler = require('scheduler');
|
||||||
act = require('internal-test-utils').act;
|
act = require('internal-test-utils').act;
|
||||||
|
assertConsoleErrorDev =
|
||||||
|
require('internal-test-utils').assertConsoleErrorDev;
|
||||||
|
|
||||||
const InternalTestUtils = require('internal-test-utils');
|
const InternalTestUtils = require('internal-test-utils');
|
||||||
assertLog = InternalTestUtils.assertLog;
|
assertLog = InternalTestUtils.assertLog;
|
||||||
|
|
@ -1237,11 +1240,15 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||||
<BrokenRender />
|
<BrokenRender />
|
||||||
</ErrorBoundary>,
|
</ErrorBoundary>,
|
||||||
);
|
);
|
||||||
await expect(async () => await waitForAll([])).toErrorDev([
|
await waitForAll([]);
|
||||||
'React.jsx: type is invalid -- expected a string',
|
if (gate(flags => !flags.enableOwnerStacks)) {
|
||||||
// React retries once on error
|
assertConsoleErrorDev([
|
||||||
'React.jsx: type is invalid -- expected a string',
|
'React.jsx: type is invalid -- expected a string',
|
||||||
]);
|
// React retries once on error
|
||||||
|
'React.jsx: type is invalid -- expected a string',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
expect(ReactNoop).toMatchRenderedOutput(
|
expect(ReactNoop).toMatchRenderedOutput(
|
||||||
<span
|
<span
|
||||||
prop={
|
prop={
|
||||||
|
|
@ -1288,11 +1295,14 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||||
<BrokenRender fail={true} />
|
<BrokenRender fail={true} />
|
||||||
</ErrorBoundary>,
|
</ErrorBoundary>,
|
||||||
);
|
);
|
||||||
await expect(async () => await waitForAll([])).toErrorDev([
|
await waitForAll([]);
|
||||||
'React.jsx: type is invalid -- expected a string',
|
if (gate(flags => !flags.enableOwnerStacks)) {
|
||||||
// React retries once on error
|
assertConsoleErrorDev([
|
||||||
'React.jsx: type is invalid -- expected a string',
|
'React.jsx: type is invalid -- expected a string',
|
||||||
]);
|
// React retries once on error
|
||||||
|
'React.jsx: type is invalid -- expected a string',
|
||||||
|
]);
|
||||||
|
}
|
||||||
expect(ReactNoop).toMatchRenderedOutput(
|
expect(ReactNoop).toMatchRenderedOutput(
|
||||||
<span
|
<span
|
||||||
prop={
|
prop={
|
||||||
|
|
@ -1310,10 +1320,14 @@ describe('ReactIncrementalErrorHandling', () => {
|
||||||
|
|
||||||
it('recovers from uncaught reconciler errors', async () => {
|
it('recovers from uncaught reconciler errors', async () => {
|
||||||
const InvalidType = undefined;
|
const InvalidType = undefined;
|
||||||
expect(() => ReactNoop.render(<InvalidType />)).toErrorDev(
|
ReactNoop.render(<InvalidType />);
|
||||||
'React.jsx: type is invalid -- expected a string',
|
if (gate(flags => !flags.enableOwnerStacks)) {
|
||||||
{withoutStack: true},
|
assertConsoleErrorDev(
|
||||||
);
|
['React.jsx: type is invalid -- expected a string'],
|
||||||
|
{withoutStack: true},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await waitForThrow(
|
await waitForThrow(
|
||||||
'Element type is invalid: expected a string (for built-in components) or ' +
|
'Element type is invalid: expected a string (for built-in components) or ' +
|
||||||
'a class/function (for composite components) but got: undefined.' +
|
'a class/function (for composite components) but got: undefined.' +
|
||||||
|
|
|
||||||
91
packages/react-server/src/ReactFlightServer.js
vendored
91
packages/react-server/src/ReactFlightServer.js
vendored
|
|
@ -110,7 +110,6 @@ import {
|
||||||
} from 'shared/ReactSymbols';
|
} from 'shared/ReactSymbols';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
describeValueForErrorMessage,
|
|
||||||
describeObjectForErrorMessage,
|
describeObjectForErrorMessage,
|
||||||
isSimpleObject,
|
isSimpleObject,
|
||||||
jsxPropsParents,
|
jsxPropsParents,
|
||||||
|
|
@ -1501,19 +1500,11 @@ function renderElement(
|
||||||
jsxChildrenParents.set(props.children, type);
|
jsxChildrenParents.set(props.children, type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeof type === 'function') {
|
if (
|
||||||
if (isClientReference(type) || isOpaqueTemporaryReference(type)) {
|
typeof type === 'function' &&
|
||||||
// This is a reference to a Client Component.
|
!isClientReference(type) &&
|
||||||
return renderClientElement(
|
!isOpaqueTemporaryReference(type)
|
||||||
task,
|
) {
|
||||||
type,
|
|
||||||
key,
|
|
||||||
props,
|
|
||||||
owner,
|
|
||||||
stack,
|
|
||||||
validated,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// This is a Server Component.
|
// This is a Server Component.
|
||||||
return renderFunctionComponent(
|
return renderFunctionComponent(
|
||||||
request,
|
request,
|
||||||
|
|
@ -1525,43 +1516,27 @@ function renderElement(
|
||||||
stack,
|
stack,
|
||||||
validated,
|
validated,
|
||||||
);
|
);
|
||||||
} else if (typeof type === 'string') {
|
} else if (type === REACT_FRAGMENT_TYPE && key === null) {
|
||||||
// This is a host element. E.g. HTML.
|
// For key-less fragments, we add a small optimization to avoid serializing
|
||||||
return renderClientElement(task, type, key, props, owner, stack, validated);
|
// it as a wrapper.
|
||||||
} else if (typeof type === 'symbol') {
|
const prevImplicitSlot = task.implicitSlot;
|
||||||
if (type === REACT_FRAGMENT_TYPE && key === null) {
|
if (task.keyPath === null) {
|
||||||
// For key-less fragments, we add a small optimization to avoid serializing
|
task.implicitSlot = true;
|
||||||
// it as a wrapper.
|
|
||||||
const prevImplicitSlot = task.implicitSlot;
|
|
||||||
if (task.keyPath === null) {
|
|
||||||
task.implicitSlot = true;
|
|
||||||
}
|
|
||||||
const json = renderModelDestructive(
|
|
||||||
request,
|
|
||||||
task,
|
|
||||||
emptyRoot,
|
|
||||||
'',
|
|
||||||
props.children,
|
|
||||||
);
|
|
||||||
task.implicitSlot = prevImplicitSlot;
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
// This might be a built-in React component. We'll let the client decide.
|
|
||||||
// Any built-in works as long as its props are serializable.
|
|
||||||
return renderClientElement(task, type, key, props, owner, stack, validated);
|
|
||||||
} else if (type != null && typeof type === 'object') {
|
|
||||||
if (isClientReference(type)) {
|
|
||||||
// This is a reference to a Client Component.
|
|
||||||
return renderClientElement(
|
|
||||||
task,
|
|
||||||
type,
|
|
||||||
key,
|
|
||||||
props,
|
|
||||||
owner,
|
|
||||||
stack,
|
|
||||||
validated,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
const json = renderModelDestructive(
|
||||||
|
request,
|
||||||
|
task,
|
||||||
|
emptyRoot,
|
||||||
|
'',
|
||||||
|
props.children,
|
||||||
|
);
|
||||||
|
task.implicitSlot = prevImplicitSlot;
|
||||||
|
return json;
|
||||||
|
} else if (
|
||||||
|
type != null &&
|
||||||
|
typeof type === 'object' &&
|
||||||
|
!isClientReference(type)
|
||||||
|
) {
|
||||||
switch (type.$$typeof) {
|
switch (type.$$typeof) {
|
||||||
case REACT_LAZY_TYPE: {
|
case REACT_LAZY_TYPE: {
|
||||||
let wrappedType;
|
let wrappedType;
|
||||||
|
|
@ -1615,11 +1590,21 @@ function renderElement(
|
||||||
validated,
|
validated,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case REACT_ELEMENT_TYPE: {
|
||||||
|
// This is invalid but we'll let the client determine that it is.
|
||||||
|
if (__DEV__) {
|
||||||
|
// Disable the key warning that would happen otherwise because this
|
||||||
|
// element gets serialized inside an array. We'll error later anyway.
|
||||||
|
type._store.validated = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(
|
// For anything else, try it on the client instead.
|
||||||
`Unsupported Server Component type: ${describeValueForErrorMessage(type)}`,
|
// We don't know if the client will support it or not. This might error on the
|
||||||
);
|
// client or error during serialization but the stack will point back to the
|
||||||
|
// server.
|
||||||
|
return renderClientElement(task, type, key, props, owner, stack, validated);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pingTask(request: Request, task: Task): void {
|
function pingTask(request: Request, task: Task): void {
|
||||||
|
|
|
||||||
|
|
@ -515,11 +515,15 @@ describe('ReactElementValidator', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
void (<Foo>{[<div />]}</Foo>);
|
void (<Foo>{[<div />]}</Foo>);
|
||||||
}).toErrorDev(
|
}).toErrorDev(
|
||||||
'React.jsx: type is invalid -- expected a string ' +
|
gate(flags => flags.enableOwnerStacks)
|
||||||
'(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, or you might have mixed up " +
|
'React.jsx: type is invalid -- expected a string ' +
|
||||||
'default and named imports.',
|
'(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, or you might have mixed up " +
|
||||||
|
'default and named imports.',
|
||||||
|
],
|
||||||
{withoutStack: true},
|
{withoutStack: true},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -215,35 +215,6 @@ describe('ReactJSXElementValidator', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gives a helpful error when passing null, undefined, or boolean', () => {
|
|
||||||
const Undefined = undefined;
|
|
||||||
const Null = null;
|
|
||||||
const True = true;
|
|
||||||
const Div = 'div';
|
|
||||||
expect(() => void (<Undefined />)).toErrorDev(
|
|
||||||
'React.jsx: 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, or you might have mixed up " +
|
|
||||||
'default and named imports.',
|
|
||||||
{withoutStack: true},
|
|
||||||
);
|
|
||||||
expect(() => void (<Null />)).toErrorDev(
|
|
||||||
'React.jsx: type is invalid -- expected a string ' +
|
|
||||||
'(for built-in components) or a class/function (for composite ' +
|
|
||||||
'components) but got: null.',
|
|
||||||
{withoutStack: true},
|
|
||||||
);
|
|
||||||
expect(() => void (<True />)).toErrorDev(
|
|
||||||
'React.jsx: type is invalid -- expected a string ' +
|
|
||||||
'(for built-in components) or a class/function (for composite ' +
|
|
||||||
'components) but got: boolean.',
|
|
||||||
{withoutStack: true},
|
|
||||||
);
|
|
||||||
// No error expected
|
|
||||||
void (<Div />);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('warns for fragments with illegal attributes', async () => {
|
it('warns for fragments with illegal attributes', async () => {
|
||||||
class Foo extends React.Component {
|
class Foo extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
|
|
|
||||||
|
|
@ -559,9 +559,14 @@ function jsxDEVImpl(
|
||||||
debugTask,
|
debugTask,
|
||||||
) {
|
) {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
if (!isValidElementType(type)) {
|
if (!enableOwnerStacks && !isValidElementType(type)) {
|
||||||
// This is an invalid element type.
|
// This is an invalid element type.
|
||||||
//
|
//
|
||||||
|
// We warn here so that we can get better stack traces but with enableOwnerStacks
|
||||||
|
// enabled we don't need this because we get good stacks if we error in the
|
||||||
|
// renderer anyway. The renderer is the only one that knows what types are valid
|
||||||
|
// for this particular renderer so we let it error there instead.
|
||||||
|
//
|
||||||
// We warn in this case but don't throw. We expect the element creation to
|
// We warn in this case but don't throw. We expect the element creation to
|
||||||
// succeed and there will likely be errors in render.
|
// succeed and there will likely be errors in render.
|
||||||
let info = '';
|
let info = '';
|
||||||
|
|
@ -604,6 +609,9 @@ function jsxDEVImpl(
|
||||||
// errors. We don't want exception behavior to differ between dev and
|
// errors. We don't want exception behavior to differ between dev and
|
||||||
// prod. (Rendering will throw with a helpful message and as soon as the
|
// prod. (Rendering will throw with a helpful message and as soon as the
|
||||||
// type is fixed, the key warnings will appear.)
|
// type is fixed, the key warnings will appear.)
|
||||||
|
// When enableOwnerStacks is on, we no longer need the type here so this
|
||||||
|
// comment is no longer true. Which is why we can run this even for invalid
|
||||||
|
// types.
|
||||||
const children = config.children;
|
const children = config.children;
|
||||||
if (children !== undefined) {
|
if (children !== undefined) {
|
||||||
if (isStaticChildren) {
|
if (isStaticChildren) {
|
||||||
|
|
@ -1103,6 +1111,17 @@ export function cloneElement(element, config, children) {
|
||||||
*/
|
*/
|
||||||
function validateChildKeys(node, parentType) {
|
function validateChildKeys(node, parentType) {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
|
if (enableOwnerStacks) {
|
||||||
|
// When owner stacks is enabled no warnings happens. All we do is
|
||||||
|
// mark elements as being in a valid static child position so they
|
||||||
|
// don't need keys.
|
||||||
|
if (isValidElement(node)) {
|
||||||
|
if (node._store) {
|
||||||
|
node._store.validated = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (typeof node !== 'object' || !node) {
|
if (typeof node !== 'object' || !node) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user