Skip to content

Move createElement/JSX Warnings into the Renderer #29088

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 23, 2024
9 changes: 6 additions & 3 deletions fixtures/flight/server/region.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,20 @@ async function renderApp(res, returnValue, formState) {
).main.css;
}
const App = m.default.default || m.default;
const root = [
const root = React.createElement(
React.Fragment,
null,
// Prepend the App's tree with stylesheets required for this entrypoint.
mainCSSChunks.map(filename =>
React.createElement('link', {
rel: 'stylesheet',
href: filename,
precedence: 'default',
key: filename,
})
),
React.createElement(App),
];
React.createElement(App)
);
// For client-invoked server actions we refresh the tree and return a return value.
const payload = {root, returnValue, formState};
const {pipe} = renderToPipeableStream(payload, moduleMap);
Expand Down
8 changes: 5 additions & 3 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ function createElement(
props: mixed,
owner: null | ReactComponentInfo, // DEV-only
stack: null | string, // DEV-only
validated: number, // DEV-only
): React$Element<any> {
let element: any;
if (__DEV__ && enableRefAsProp) {
Expand Down Expand Up @@ -624,13 +625,13 @@ function createElement(
// Unfortunately, _store is enumerable in jest matchers so for equality to
// work, I need to keep it or make _store non-enumerable in the other file.
element._store = ({}: {
validated?: boolean,
validated?: number,
});
Object.defineProperty(element._store, 'validated', {
configurable: false,
enumerable: false,
writable: true,
value: true, // This element has already been validated on the server.
value: enableOwnerStacks ? validated : 1, // Whether the element has already been validated on the server.
});
// debugInfo contains Server Component debug information.
Object.defineProperty(element, '_debugInfo', {
Expand All @@ -644,7 +645,7 @@ function createElement(
configurable: false,
enumerable: false,
writable: true,
value: {stack: stack},
value: stack,
});
Object.defineProperty(element, '_debugTask', {
configurable: false,
Expand Down Expand Up @@ -1053,6 +1054,7 @@ function parseModelTuple(
tuple[3],
__DEV__ ? (tuple: any)[4] : null,
__DEV__ && enableOwnerStacks ? (tuple: any)[5] : null,
__DEV__ && enableOwnerStacks ? (tuple: any)[6] : 0,
);
}
return value;
Expand Down
40 changes: 32 additions & 8 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,9 @@ describe('ReactFlight', () => {
'\n' +
'Check the render method of `Component`. See https://react.dev/link/warning-keys for more information.\n' +
' in span (at **)\n' +
// TODO: Because this validates after the div has been mounted, it is part of
// the parent stack but since owner stacks will switch to owners this goes away again.
(gate(flags => flags.enableOwnerStacks) ? ' in div (at **)\n' : '') +
' in Component (at **)\n' +
' in Indirection (at **)\n' +
' in App (at **)',
Expand Down Expand Up @@ -1386,19 +1389,40 @@ describe('ReactFlight', () => {
ReactNoopFlightClient.read(transport);
});

it('should warn in DEV a child is missing keys', () => {
it('should warn in DEV a child is missing keys on server component', () => {
function NoKey({children}) {
return <div key="this has a key but parent doesn't" />;
}
expect(() => {
const transport = ReactNoopFlightServer.render(
<div>{Array(6).fill(<NoKey />)}</div>,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev('Each child in a list should have a unique "key" prop.', {
withoutStack: gate(flags => flags.enableOwnerStacks),
});
});

it('should warn in DEV a child is missing keys in client component', async () => {
function ParentClient({children}) {
return children;
}
const Parent = clientReference(ParentClient);
expect(() => {
await expect(async () => {
const transport = ReactNoopFlightServer.render(
<Parent>{Array(6).fill(<div>no key</div>)}</Parent>,
);
ReactNoopFlightClient.read(transport);
await act(async () => {
ReactNoop.render(await ReactNoopFlightClient.read(transport));
});
}).toErrorDev(
'Each child in a list should have a unique "key" prop. ' +
'See https://react.dev/link/warning-keys for more information.',
gate(flags => flags.enableOwnerStacks)
? 'Each child in a list should have a unique "key" prop.' +
'\n\nCheck the top-level render call using <ParentClient>. ' +
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got confused by this for a bit. I thought it suggested I should look inside ParentClient.

What do you think about

Suggested change
'\n\nCheck the top-level render call using <ParentClient>. ' +
'\n\nCheck the root.render() call using <ParentClient>. ' +

?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to remove this whole contextual thing once we ship the owner stacks. It makes no sense and is kind of an artifact of trying to explain the actual cause which we don't have. I just didn't want to change it yet to keep parity in the incremental steps. Mainly because of all the tests. The current PR I'm working on I'm manually editing hundreds of tests. I'm actually doing a lot of incremental stuff I wouldn't do just because editing our tests is too much work.

'See https://react.dev/link/warning-keys for more information.'
: 'Each child in a list should have a unique "key" prop. ' +
'See https://react.dev/link/warning-keys for more information.',
);
});

Expand Down Expand Up @@ -2306,7 +2330,7 @@ describe('ReactFlight', () => {
}

function ThirdPartyFragmentComponent() {
return [<span>Who</span>, ' ', <span>dis?</span>];
return [<span key="1">Who</span>, ' ', <span key="2">dis?</span>];
}

function ServerComponent({transport}) {
Expand All @@ -2318,7 +2342,7 @@ describe('ReactFlight', () => {
const promiseComponent = Promise.resolve(<ThirdPartyComponent />);

const thirdPartyTransport = ReactNoopFlightServer.render(
[promiseComponent, lazy, <ThirdPartyFragmentComponent />],
[promiseComponent, lazy, <ThirdPartyFragmentComponent key="3" />],
{
environmentName: 'third-party',
},
Expand Down Expand Up @@ -2410,8 +2434,8 @@ describe('ReactFlight', () => {
const iteratorPromise = new Promise(r => (resolve = r));

async function* ThirdPartyAsyncIterableComponent({item, initial}) {
yield <span>Who</span>;
yield <span>dis?</span>;
yield <span key="1">Who</span>;
yield <span key="2">dis?</span>;
resolve();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2562,13 +2562,7 @@ describe('InspectedElement', () => {
const data = await getErrorsAndWarningsForElementAtIndex(0);
expect(data).toMatchInlineSnapshot(`
{
"errors": [
[
"Warning: Each child in a list should have a unique "key" prop. See https://react.dev/link/warning-keys for more information.
at Example",
1,
],
],
"errors": [],
"warnings": [],
}
`);
Expand Down
3 changes: 1 addition & 2 deletions packages/react-devtools-shared/src/__tests__/store-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1932,9 +1932,8 @@ describe('Store', () => {
);

expect(store).toMatchInlineSnapshot(`
✕ 1, ⚠ 0
[root]
▾ <Example>
▾ <Example>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance we can get these back? It was part of the feature that you could jump to the element in devtools to find the offending element quickly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea. So this is directly related to this TODO:

// TODO: Refactor the warnForMissingKey calls to happen after fiber creation
// so that we can get access to the fiber that will eventually be created.
// That way the log can show up associated with the right instance in DevTools.

Previously it was the "owner" of the parent which sometimes isn't related to where these elements are created at all. Now it's the actual child that should have the key specified on it but since that's a fake fiber it is not persistent in DevTools. We need to refactor ChildFiber to allow this warning to happen when we have access to the real fiber and it'll be better for context but I'm not planning on doing that yet because the rest of it is too much work already.

<Child>
`);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,8 @@
// while still maintaining support for multiple renderer versions
// (which use different values for ReactTypeOfWork).

import type {LazyComponent} from 'react/src/ReactLazy';
import type {CurrentDispatcherRef} from './types';

import {
FORWARD_REF_NUMBER,
FORWARD_REF_SYMBOL_STRING,
LAZY_NUMBER,
LAZY_SYMBOL_STRING,
MEMO_NUMBER,
MEMO_SYMBOL_STRING,
SUSPENSE_NUMBER,
SUSPENSE_SYMBOL_STRING,
SUSPENSE_LIST_NUMBER,
SUSPENSE_LIST_SYMBOL_STRING,
} from './ReactSymbols';

// The shared console patching code is DEV-only.
// We can't use it since DevTools only ships production builds.
import {disableLogs, reenableLogs} from './DevToolsConsolePatching';
Expand Down Expand Up @@ -297,69 +283,3 @@ export function describeFunctionComponentFrame(
): string {
return describeNativeComponentFrame(fn, false, currentDispatcherRef);
}

function shouldConstruct(Component: Function) {
const prototype = Component.prototype;
return !!(prototype && prototype.isReactComponent);
}

export function describeUnknownElementTypeFrameInDEV(
type: any,
currentDispatcherRef: CurrentDispatcherRef,
): string {
if (!__DEV__) {
return '';
}
if (type == null) {
return '';
}
if (typeof type === 'function') {
return describeNativeComponentFrame(
type,
shouldConstruct(type),
currentDispatcherRef,
);
}
if (typeof type === 'string') {
return describeBuiltInComponentFrame(type);
}
switch (type) {
case SUSPENSE_NUMBER:
case SUSPENSE_SYMBOL_STRING:
return describeBuiltInComponentFrame('Suspense');
case SUSPENSE_LIST_NUMBER:
case SUSPENSE_LIST_SYMBOL_STRING:
return describeBuiltInComponentFrame('SuspenseList');
}
if (typeof type === 'object') {
switch (type.$$typeof) {
case FORWARD_REF_NUMBER:
case FORWARD_REF_SYMBOL_STRING:
return describeFunctionComponentFrame(
type.render,
currentDispatcherRef,
);
case MEMO_NUMBER:
case MEMO_SYMBOL_STRING:
// Memo may contain any component type so we recursively resolve it.
return describeUnknownElementTypeFrameInDEV(
type.type,
currentDispatcherRef,
);
case LAZY_NUMBER:
case LAZY_SYMBOL_STRING: {
const lazyComponent: LazyComponent<any, any> = (type: any);
const payload = lazyComponent._payload;
const init = lazyComponent._init;
try {
// Lazy may contain any component type so we recursively resolve it.
return describeUnknownElementTypeFrameInDEV(
init(payload),
currentDispatcherRef,
);
} catch (x) {}
}
}
}
return '';
}
25 changes: 22 additions & 3 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1833,10 +1833,18 @@ describe('ReactDOMFizzServer', () => {
expect(mockError).toHaveBeenCalledWith(
'Warning: Each child in a list should have a unique "key" prop.%s%s' +
' See https://react.dev/link/warning-keys for more information.%s',
'\n\nCheck the top-level render call using <div>.',
gate(flags => flags.enableOwnerStacks)
? // We currently don't track owners in Fizz which is responsible for this frame.
''
: '\n\nCheck the top-level render call using <div>.',
'',
'\n' +
' in span (at **)\n' +
// TODO: Because this validates after the div has been mounted, it is part of
// the parent stack but since owner stacks will switch to owners this goes away again.
(gate(flags => flags.enableOwnerStacks)
? ' in div (at **)\n'
: '') +
' in B (at **)\n' +
' in Suspense (at **)\n' +
' in div (at **)\n' +
Expand Down Expand Up @@ -1890,7 +1898,12 @@ describe('ReactDOMFizzServer', () => {
</b>
);
if (this.props.prefix) {
return [readText(this.props.prefix), child];
return (
<>
{readText(this.props.prefix)}
{child}
</>
);
}
return child;
}
Expand All @@ -1900,7 +1913,13 @@ describe('ReactDOMFizzServer', () => {
const {pipe} = renderToPipeableStream(
<TestProvider ctx="A">
<div>
<Suspense fallback={[<Text text="Loading: " />, <TestConsumer />]}>
<Suspense
fallback={
<>
<Text text="Loading: " />
<TestConsumer />
</>
}>
<TestProvider ctx="B">
<TestConsumer prefix="Hello: " />
</TestProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,7 @@ describe('ReactDOMServer', () => {
}

function Child() {
return [<A key="1" />, <B key="2" />, <span ariaTypo2="no" />];
return [<A key="1" />, <B key="2" />, <span ariaTypo2="no" key="3" />];
}

function App() {
Expand Down
Loading