Skip to content

Commit 19bd26b

Browse files
authored
[Flight/DevTools] Pass the Server Component's "key" as Part of the ReactComponentInfo (#30703)
Supports showing the key in DevTools on the Server Component that the key was applied to. We can also use this to reconcile to preserve instance equality when they're reordered. One thing that's a bit weird about this is that if you provide an explicit key on a Server Component that alone doesn't have any semantics. It's because we pass the key down and let the nearest child inherit the key or get prefixed by the key. So you might see the same key as a prefix on the child of the Server Component too which might be a bit confusing. We could remove the prefix from children but that might also be a bit confusing if they collide. The div in this case doesn't have a key explicitly specified. It gets it from the Server Component parent. <img width="1107" alt="Screenshot 2024-08-14 at 10 06 36 PM" src="https://github.com/user-attachments/assets/cfc517cc-e737-44c3-a1be-050049267ee2"> Overall keys get a bit confusing when you apply filter. Especially since it's so common to actually apply the key on a Host Instance. So you often don't see the key.
1 parent c39da3e commit 19bd26b

File tree

6 files changed

+64
-27
lines changed

6 files changed

+64
-27
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ describe('ReactFlight', () => {
299299
{
300300
name: 'Greeting',
301301
env: 'Server',
302+
key: null,
302303
owner: null,
303304
stack: gate(flag => flag.enableOwnerStacks)
304305
? ' in Object.<anonymous> (at **)'
@@ -337,6 +338,7 @@ describe('ReactFlight', () => {
337338
{
338339
name: 'Greeting',
339340
env: 'Server',
341+
key: null,
340342
owner: null,
341343
stack: gate(flag => flag.enableOwnerStacks)
342344
? ' in Object.<anonymous> (at **)'
@@ -2614,6 +2616,7 @@ describe('ReactFlight', () => {
26142616
{
26152617
name: 'ServerComponent',
26162618
env: 'Server',
2619+
key: null,
26172620
owner: null,
26182621
stack: gate(flag => flag.enableOwnerStacks)
26192622
? ' in Object.<anonymous> (at **)'
@@ -2631,6 +2634,7 @@ describe('ReactFlight', () => {
26312634
{
26322635
name: 'ThirdPartyComponent',
26332636
env: 'third-party',
2637+
key: null,
26342638
owner: null,
26352639
stack: gate(flag => flag.enableOwnerStacks)
26362640
? ' in Object.<anonymous> (at **)'
@@ -2645,6 +2649,7 @@ describe('ReactFlight', () => {
26452649
{
26462650
name: 'ThirdPartyLazyComponent',
26472651
env: 'third-party',
2652+
key: null,
26482653
owner: null,
26492654
stack: gate(flag => flag.enableOwnerStacks)
26502655
? ' in myLazy (at **)\n in lazyInitializer (at **)'
@@ -2659,6 +2664,7 @@ describe('ReactFlight', () => {
26592664
{
26602665
name: 'ThirdPartyFragmentComponent',
26612666
env: 'third-party',
2667+
key: '3',
26622668
owner: null,
26632669
stack: gate(flag => flag.enableOwnerStacks)
26642670
? ' in Object.<anonymous> (at **)'
@@ -2732,6 +2738,7 @@ describe('ReactFlight', () => {
27322738
{
27332739
name: 'ServerComponent',
27342740
env: 'Server',
2741+
key: null,
27352742
owner: null,
27362743
stack: gate(flag => flag.enableOwnerStacks)
27372744
? ' in Object.<anonymous> (at **)'
@@ -2748,6 +2755,7 @@ describe('ReactFlight', () => {
27482755
{
27492756
name: 'Keyed',
27502757
env: 'Server',
2758+
key: 'keyed',
27512759
owner: null,
27522760
stack: gate(flag => flag.enableOwnerStacks)
27532761
? ' in ServerComponent (at **)'
@@ -2763,6 +2771,7 @@ describe('ReactFlight', () => {
27632771
{
27642772
name: 'ThirdPartyAsyncIterableComponent',
27652773
env: 'third-party',
2774+
key: null,
27662775
owner: null,
27672776
stack: gate(flag => flag.enableOwnerStacks)
27682777
? ' in Object.<anonymous> (at **)'
@@ -2920,6 +2929,7 @@ describe('ReactFlight', () => {
29202929
{
29212930
name: 'Component',
29222931
env: 'A',
2932+
key: null,
29232933
owner: null,
29242934
stack: gate(flag => flag.enableOwnerStacks)
29252935
? ' in Object.<anonymous> (at **)'
@@ -3040,6 +3050,7 @@ describe('ReactFlight', () => {
30403050
const greetInfo = {
30413051
name: 'Greeting',
30423052
env: 'Server',
3053+
key: null,
30433054
owner: null,
30443055
stack: gate(flag => flag.enableOwnerStacks)
30453056
? ' in Object.<anonymous> (at **)'
@@ -3050,6 +3061,7 @@ describe('ReactFlight', () => {
30503061
{
30513062
name: 'Container',
30523063
env: 'Server',
3064+
key: null,
30533065
owner: greetInfo,
30543066
stack: gate(flag => flag.enableOwnerStacks)
30553067
? ' in Greeting (at **)'

packages/react-devtools-shared/src/__tests__/store-test.js

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2439,7 +2439,7 @@ describe('Store', () => {
24392439
});
24402440

24412441
// @reactVersion > 18.2
2442-
it('can reorder keyed components', async () => {
2442+
it('can reorder keyed server components', async () => {
24432443
function ClientComponent({text}) {
24442444
return <div>{text}</div>;
24452445
}
@@ -2452,9 +2452,7 @@ describe('Store', () => {
24522452
name: 'ServerComponent',
24532453
env: 'Server',
24542454
owner: null,
2455-
// TODO: Ideally the debug info should include the "key" too to
2456-
// preserve the virtual identity of the server component when
2457-
// reordered. Atm only the children of it gets reparented.
2455+
key: key,
24582456
},
24592457
];
24602458
return ServerPromise;
@@ -2468,23 +2466,23 @@ describe('Store', () => {
24682466
expect(store).toMatchInlineSnapshot(`
24692467
[root]
24702468
▾ <App>
2471-
▾ <ServerComponent> [Server]
2469+
▾ <ServerComponent key="A"> [Server]
24722470
<ClientComponent key="A">
2473-
▾ <ServerComponent> [Server]
2471+
▾ <ServerComponent key="B"> [Server]
24742472
<ClientComponent key="B">
2475-
▾ <ServerComponent> [Server]
2473+
▾ <ServerComponent key="C"> [Server]
24762474
<ClientComponent key="C">
24772475
`);
24782476

24792477
await actAsync(() => render(<App initial={false} />));
24802478
expect(store).toMatchInlineSnapshot(`
24812479
[root]
24822480
▾ <App>
2483-
▾ <ServerComponent> [Server]
2481+
▾ <ServerComponent key="B"> [Server]
24842482
<ClientComponent key="B">
2485-
▾ <ServerComponent> [Server]
2483+
▾ <ServerComponent key="A"> [Server]
24862484
<ClientComponent key="A">
2487-
▾ <ServerComponent> [Server]
2485+
▾ <ServerComponent key="D"> [Server]
24882486
<ClientComponent key="D">
24892487
`);
24902488
});

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2220,9 +2220,12 @@ export function attach(
22202220

22212221
const isProfilingSupported = false; // TODO: Support Tree Base Duration Based on Children.
22222222

2223-
const key = null; // TODO: Track keys on ReactComponentInfo;
2224-
const env = instance.data.env;
2225-
let displayName = instance.data.name || '';
2223+
const componentInfo = instance.data;
2224+
2225+
const key =
2226+
typeof componentInfo.key === 'string' ? componentInfo.key : null;
2227+
const env = componentInfo.env;
2228+
let displayName = componentInfo.name || '';
22262229
if (typeof env === 'string') {
22272230
// We model environment as an HoC name for now.
22282231
displayName = env + '(' + displayName + ')';
@@ -2855,19 +2858,35 @@ export function attach(
28552858
);
28562859
}
28572860
}
2858-
const firstRemainingChild = remainingReconcilingChildren;
2861+
// TODO: Find the best matching existing child based on the key if defined.
2862+
2863+
let bestMatch = remainingReconcilingChildren;
2864+
if (componentInfo.key != null) {
2865+
// If there is a key try to find a matching key in the set.
2866+
bestMatch = remainingReconcilingChildren;
2867+
while (bestMatch !== null) {
2868+
if (
2869+
bestMatch.kind === VIRTUAL_INSTANCE &&
2870+
bestMatch.data.key === componentInfo.key
2871+
) {
2872+
break;
2873+
}
2874+
bestMatch = bestMatch.nextSibling;
2875+
}
2876+
}
28592877
if (
2860-
firstRemainingChild !== null &&
2861-
firstRemainingChild.kind === VIRTUAL_INSTANCE &&
2862-
firstRemainingChild.data.name === componentInfo.name &&
2863-
firstRemainingChild.data.env === componentInfo.env
2878+
bestMatch !== null &&
2879+
bestMatch.kind === VIRTUAL_INSTANCE &&
2880+
bestMatch.data.name === componentInfo.name &&
2881+
bestMatch.data.env === componentInfo.env &&
2882+
bestMatch.data.key === componentInfo.key
28642883
) {
28652884
// If the previous children had a virtual instance in the same slot
28662885
// with the same name, then we claim it and reuse it for this update.
28672886
// Update it with the latest entry.
2868-
firstRemainingChild.data = componentInfo;
2869-
moveChild(firstRemainingChild);
2870-
previousVirtualInstance = firstRemainingChild;
2887+
bestMatch.data = componentInfo;
2888+
moveChild(bestMatch);
2889+
previousVirtualInstance = bestMatch;
28712890
previousVirtualInstanceWasMount = false;
28722891
} else {
28732892
// Otherwise we create a new instance.
@@ -4321,11 +4340,13 @@ export function attach(
43214340
): InspectedElement | null {
43224341
const canViewSource = false;
43234342

4324-
const key = null; // TODO: Track keys on ReactComponentInfo;
4343+
const componentInfo = virtualInstance.data;
4344+
const key =
4345+
typeof componentInfo.key === 'string' ? componentInfo.key : null;
43254346
const props = null; // TODO: Track props on ReactComponentInfo;
43264347

4327-
const env = virtualInstance.data.env;
4328-
let displayName = virtualInstance.data.name || '';
4348+
const env = componentInfo.env;
4349+
let displayName = componentInfo.name || '';
43294350
if (typeof env === 'string') {
43304351
// We model environment as an HoC name for now.
43314352
displayName = env + '(' + displayName + ')';
@@ -4384,7 +4405,7 @@ export function attach(
43844405
// Does the component have legacy context attached to it.
43854406
hasLegacyContext: false,
43864407

4387-
key: key != null ? key : null,
4408+
key: key,
43884409

43894410
displayName: displayName,
43904411
type: ElementTypeVirtual,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ describe('ReactFlightDOMEdge', () => {
307307

308308
const serializedContent = await readResult(stream1);
309309

310-
expect(serializedContent.length).toBeLessThan(410);
310+
expect(serializedContent.length).toBeLessThan(425);
311311
expect(timesRendered).toBeLessThan(5);
312312

313313
const model = await ReactServerDOMClient.createFromReadableStream(stream2, {
@@ -374,7 +374,7 @@ describe('ReactFlightDOMEdge', () => {
374374
const [stream1, stream2] = passThrough(stream).tee();
375375

376376
const serializedContent = await readResult(stream1);
377-
expect(serializedContent.length).toBeLessThan(__DEV__ ? 590 : 400);
377+
expect(serializedContent.length).toBeLessThan(__DEV__ ? 605 : 400);
378378
expect(timesRendered).toBeLessThan(5);
379379

380380
const model = await ReactServerDOMClient.createFromReadableStream(stream2, {

packages/react-server/src/ReactFlightServer.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,7 @@ function callWithDebugContextInDEV<A, T>(
970970
const componentDebugInfo: ReactComponentInfo = {
971971
name: '',
972972
env: task.environmentName,
973+
key: null,
973974
owner: task.debugOwner,
974975
};
975976
if (enableOwnerStacks) {
@@ -1036,6 +1037,7 @@ function renderFunctionComponent<Props>(
10361037
componentDebugInfo = ({
10371038
name: componentName,
10381039
env: componentEnv,
1040+
key: key,
10391041
owner: task.debugOwner,
10401042
}: ReactComponentInfo);
10411043
if (enableOwnerStacks) {
@@ -1575,6 +1577,7 @@ function renderElement(
15751577
const componentDebugInfo: ReactComponentInfo = {
15761578
name: 'Fragment',
15771579
env: (0, request.environmentName)(),
1580+
key: key,
15781581
owner: task.debugOwner,
15791582
stack:
15801583
task.debugStack === null
@@ -2615,6 +2618,7 @@ function renderModelDestructive(
26152618
> = {
26162619
name: (value: any).name,
26172620
env: (value: any).env,
2621+
key: (value: any).key,
26182622
owner: (value: any).owner,
26192623
};
26202624
if (enableOwnerStacks) {
@@ -3287,6 +3291,7 @@ function renderConsoleValue(
32873291
> = {
32883292
name: (value: any).name,
32893293
env: (value: any).env,
3294+
key: (value: any).key,
32903295
owner: (value: any).owner,
32913296
};
32923297
if (enableOwnerStacks) {

packages/shared/ReactTypes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export type ReactStackTrace = Array<ReactCallSite>;
190190
export type ReactComponentInfo = {
191191
+name?: string,
192192
+env?: string,
193+
+key?: null | string,
193194
+owner?: null | ReactComponentInfo,
194195
+stack?: null | ReactStackTrace,
195196
// Stashed Data for the Specific Execution Environment. Not part of the transport protocol

0 commit comments

Comments
 (0)