Skip to content

Commit 168b54e

Browse files
committed
Validate key in Flight
Elements that render Server Components are validated inside Flight. Others pass the validated flag to the client. Instead of running validation in certain cases (block list) we're marking all the cases that don't need keys (allow list) which is tricky since we need to cover all cases. This might lead to false warnings.
1 parent c62d157 commit 168b54e

File tree

5 files changed

+230
-23
lines changed

5 files changed

+230
-23
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,7 @@ function createElement(
565565
props: mixed,
566566
owner: null | ReactComponentInfo, // DEV-only
567567
stack: null | string, // DEV-only
568+
validated: number, // DEV-only
568569
): React$Element<any> {
569570
let element: any;
570571
if (__DEV__ && enableRefAsProp) {
@@ -616,7 +617,7 @@ function createElement(
616617
configurable: false,
617618
enumerable: false,
618619
writable: true,
619-
value: 1, // This element has already been validated on the server.
620+
value: enableOwnerStacks ? validated : 1, // Whether the element has already been validated on the server.
620621
});
621622
// debugInfo contains Server Component debug information.
622623
Object.defineProperty(element, '_debugInfo', {
@@ -630,7 +631,7 @@ function createElement(
630631
configurable: false,
631632
enumerable: false,
632633
writable: true,
633-
value: {stack: stack},
634+
value: stack,
634635
});
635636
Object.defineProperty(element, '_debugTask', {
636637
configurable: false,
@@ -1039,6 +1040,7 @@ function parseModelTuple(
10391040
tuple[3],
10401041
__DEV__ ? (tuple: any)[4] : null,
10411042
__DEV__ && enableOwnerStacks ? (tuple: any)[5] : null,
1043+
__DEV__ && enableOwnerStacks ? (tuple: any)[6] : 0,
10421044
);
10431045
}
10441046
return value;

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,19 +1389,40 @@ describe('ReactFlight', () => {
13891389
ReactNoopFlightClient.read(transport);
13901390
});
13911391

1392-
it('should warn in DEV a child is missing keys', () => {
1392+
it('should warn in DEV a child is missing keys on server component', () => {
1393+
function NoKey({children}) {
1394+
return <div key="this has a key but parent doesn't" />;
1395+
}
1396+
expect(() => {
1397+
const transport = ReactNoopFlightServer.render(
1398+
<div>{Array(6).fill(<NoKey />)}</div>,
1399+
);
1400+
ReactNoopFlightClient.read(transport);
1401+
}).toErrorDev('Each child in a list should have a unique "key" prop.', {
1402+
withoutStack: gate(flags => flags.enableOwnerStacks),
1403+
});
1404+
});
1405+
1406+
it('should warn in DEV a child is missing keys in client component', async () => {
13931407
function ParentClient({children}) {
13941408
return children;
13951409
}
13961410
const Parent = clientReference(ParentClient);
1397-
expect(() => {
1411+
await expect(async () => {
13981412
const transport = ReactNoopFlightServer.render(
13991413
<Parent>{Array(6).fill(<div>no key</div>)}</Parent>,
14001414
);
14011415
ReactNoopFlightClient.read(transport);
1416+
await act(async () => {
1417+
ReactNoop.render(await ReactNoopFlightClient.read(transport));
1418+
});
14021419
}).toErrorDev(
1403-
'Each child in a list should have a unique "key" prop. ' +
1404-
'See https://react.dev/link/warning-keys for more information.',
1420+
gate(flags => flags.enableOwnerStacks)
1421+
? 'Each child in a list should have a unique "key" prop.' +
1422+
'\n\nCheck the top-level render call using <ParentClient>. ' +
1423+
'See https://react.dev/link/warning-keys for more information.'
1424+
: 'Each child in a list should have a unique "key" prop. ' +
1425+
'See https://react.dev/link/warning-keys for more information.',
14051426
);
14061427
});
14071428

@@ -2309,7 +2330,7 @@ describe('ReactFlight', () => {
23092330
}
23102331

23112332
function ThirdPartyFragmentComponent() {
2312-
return [<span>Who</span>, ' ', <span>dis?</span>];
2333+
return [<span key="1">Who</span>, ' ', <span key="2">dis?</span>];
23132334
}
23142335

23152336
function ServerComponent({transport}) {
@@ -2321,7 +2342,7 @@ describe('ReactFlight', () => {
23212342
const promiseComponent = Promise.resolve(<ThirdPartyComponent />);
23222343

23232344
const thirdPartyTransport = ReactNoopFlightServer.render(
2324-
[promiseComponent, lazy, <ThirdPartyFragmentComponent />],
2345+
[promiseComponent, lazy, <ThirdPartyFragmentComponent key="3" />],
23252346
{
23262347
environmentName: 'third-party',
23272348
},
@@ -2413,8 +2434,8 @@ describe('ReactFlight', () => {
24132434
const iteratorPromise = new Promise(r => (resolve = r));
24142435

24152436
async function* ThirdPartyAsyncIterableComponent({item, initial}) {
2416-
yield <span>Who</span>;
2417-
yield <span>dis?</span>;
2437+
yield <span key="1">Who</span>;
2438+
yield <span key="2">dis?</span>;
24182439
resolve();
24192440
}
24202441

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,10 @@ describe('ReactFlightDOMBrowser', () => {
645645
}
646646
const Parent = clientExports(ParentClient);
647647
const ParentModule = clientExports({Parent: ParentClient});
648+
649+
const container = document.createElement('div');
650+
const root = ReactDOMClient.createRoot(container);
651+
648652
await expect(async () => {
649653
const stream = ReactServerDOMServer.renderToReadableStream(
650654
<>
@@ -655,11 +659,12 @@ describe('ReactFlightDOMBrowser', () => {
655659
</>,
656660
webpackMap,
657661
);
658-
await ReactServerDOMClient.createFromReadableStream(stream);
659-
}).toErrorDev(
660-
'Each child in a list should have a unique "key" prop. ' +
661-
'See https://react.dev/link/warning-keys for more information.',
662-
);
662+
const result =
663+
await ReactServerDOMClient.createFromReadableStream(stream);
664+
await act(() => {
665+
root.render(result);
666+
});
667+
}).toErrorDev('Each child in a list should have a unique "key" prop.');
663668
});
664669

665670
it('basic use(promise)', async () => {

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,41 @@ describe('ReactFlightDOMEdge', () => {
256256
return str;
257257
}
258258
const element = <ServerComponent />;
259-
const children = new Array(30).fill(element);
259+
// Hardcoded list to avoid the key warning
260+
const children = (
261+
<>
262+
{element}
263+
{element}
264+
{element}
265+
{element}
266+
{element}
267+
{element}
268+
{element}
269+
{element}
270+
{element}
271+
{element}
272+
{element}
273+
{element}
274+
{element}
275+
{element}
276+
{element}
277+
{element}
278+
{element}
279+
{element}
280+
{element}
281+
{element}
282+
{element}
283+
{element}
284+
{element}
285+
{element}
286+
{element}
287+
{element}
288+
{element}
289+
{element}
290+
{element}
291+
{element}
292+
</>
293+
);
260294
const resolvedChildren = new Array(30).fill(str);
261295
const stream = ReactServerDOMServer.renderToReadableStream(children);
262296
const [stream1, stream2] = passThrough(stream).tee();
@@ -288,7 +322,41 @@ describe('ReactFlightDOMEdge', () => {
288322
return div;
289323
}
290324
const element = <ServerComponent />;
291-
const children = new Array(30).fill(element);
325+
// Hardcoded list to avoid the key warning
326+
const children = (
327+
<>
328+
{element}
329+
{element}
330+
{element}
331+
{element}
332+
{element}
333+
{element}
334+
{element}
335+
{element}
336+
{element}
337+
{element}
338+
{element}
339+
{element}
340+
{element}
341+
{element}
342+
{element}
343+
{element}
344+
{element}
345+
{element}
346+
{element}
347+
{element}
348+
{element}
349+
{element}
350+
{element}
351+
{element}
352+
{element}
353+
{element}
354+
{element}
355+
{element}
356+
{element}
357+
{element}
358+
</>
359+
);
292360
const resolvedChildren = new Array(30).fill(
293361
'<div>this is a long return value</div>',
294362
);

0 commit comments

Comments
 (0)