Skip to content
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

[Flight] Deduplicate suspended elements #28748

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 27 additions & 16 deletions packages/react-client/src/ReactFlightClient.js
Expand Up @@ -661,6 +661,7 @@ function createModelResolver<T>(
cyclic: boolean,
response: Response,
map: (response: Response, model: any) => T,
path: Array<string>,
): (value: any) => void {
let blocked;
if (initializingChunkBlockedModel) {
Expand All @@ -675,6 +676,9 @@ function createModelResolver<T>(
};
}
return value => {
for (let i = 1; i < path.length; i++) {
value = value[path[i]];
}
parentObject[key] = map(response, value);

// If this is the root object for a model reference, where `blocked.value`
Expand Down Expand Up @@ -733,11 +737,13 @@ function createServerReferenceProxy<A: Iterable<any>, T>(

function getOutlinedModel<T>(
response: Response,
id: number,
reference: string,
parentObject: Object,
key: string,
map: (response: Response, model: any) => T,
): T {
const path = reference.split(':');
const id = parseInt(path[0], 16);
const chunk = getChunk(response, id);
switch (chunk.status) {
case RESOLVED_MODEL:
Expand All @@ -750,7 +756,11 @@ function getOutlinedModel<T>(
// The status might have changed after initialization.
switch (chunk.status) {
case INITIALIZED:
const chunkValue = map(response, chunk.value);
let value = chunk.value;
for (let i = 1; i < path.length; i++) {
value = value[path[i]];
}
const chunkValue = map(response, value);
if (__DEV__ && chunk._debugInfo) {
// If we have a direct reference to an object that was rendered by a synchronous
// server component, it might have some debug info about how it was rendered.
Expand Down Expand Up @@ -790,6 +800,7 @@ function getOutlinedModel<T>(
chunk.status === CYCLIC,
response,
map,
path,
),
createModelReject(parentChunk),
);
Expand Down Expand Up @@ -874,10 +885,10 @@ function parseModelString(
}
case 'F': {
// Server Reference
const id = parseInt(value.slice(2), 16);
const ref = value.slice(2);
return getOutlinedModel(
response,
id,
ref,
parentObject,
key,
createServerReferenceProxy,
Expand All @@ -897,39 +908,39 @@ function parseModelString(
}
case 'Q': {
// Map
const id = parseInt(value.slice(2), 16);
return getOutlinedModel(response, id, parentObject, key, createMap);
const ref = value.slice(2);
return getOutlinedModel(response, ref, parentObject, key, createMap);
}
case 'W': {
// Set
const id = parseInt(value.slice(2), 16);
return getOutlinedModel(response, id, parentObject, key, createSet);
const ref = value.slice(2);
return getOutlinedModel(response, ref, parentObject, key, createSet);
}
case 'B': {
// Blob
if (enableBinaryFlight) {
const id = parseInt(value.slice(2), 16);
return getOutlinedModel(response, id, parentObject, key, createBlob);
const ref = value.slice(2);
return getOutlinedModel(response, ref, parentObject, key, createBlob);
}
return undefined;
}
case 'K': {
// FormData
const id = parseInt(value.slice(2), 16);
const ref = value.slice(2);
return getOutlinedModel(
response,
id,
ref,
parentObject,
key,
createFormData,
);
}
case 'i': {
// Iterator
const id = parseInt(value.slice(2), 16);
const ref = value.slice(2);
return getOutlinedModel(
response,
id,
ref,
parentObject,
key,
extractIterator,
Expand Down Expand Up @@ -981,8 +992,8 @@ function parseModelString(
}
default: {
// We assume that anything else is a reference ID.
const id = parseInt(value.slice(1), 16);
return getOutlinedModel(response, id, parentObject, key, createModel);
const ref = value.slice(1);
return getOutlinedModel(response, ref, parentObject, key, createModel);
}
}
}
Expand Down
72 changes: 72 additions & 0 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Expand Up @@ -2490,4 +2490,76 @@ describe('ReactFlight', () => {

expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb</span>);
});

// @gate __DEV__
it('does not emit duplicate chunks for already outlined elements in dev mode', async () => {
async function Bar({text}) {
return text.toUpperCase();
}

function Foo() {
const bar = <Bar text="bar" />;

return (
<div>
{bar}
{bar}
</div>
);
}

const transport = ReactNoopFlightServer.render(<Foo />);
const textDecoder = new TextDecoder();

await act(async () => {
const chunks = transport
.map(chunk => textDecoder.decode(chunk).replace(/\n$/, ''))
.join('\n');

expect(chunks).toEqual(
`
1:{"name":"Foo","env":"Server","owner":null}
0:D"$1"
3:{"name":"Bar","env":"Server","owner":null}
2:D"$3"
0:["$","div",null,{"children":["$L2","$2"]},null]
2:"BAR"
`.trim(),
);
});
});

// @gate !__DEV__
it('does not emit duplicate chunks for already outlined elements in production mode', async () => {
async function Bar({text}) {
return text.toUpperCase();
}

function Foo() {
const bar = <Bar text="bar" />;

return (
<div>
{bar}
{bar}
</div>
);
}

const transport = ReactNoopFlightServer.render(<Foo />);
const textDecoder = new TextDecoder();

await act(async () => {
const chunks = transport
.map(chunk => textDecoder.decode(chunk).replace(/\n$/, ''))
.join('\n');

expect(chunks).toEqual(
`
0:["$","div",null,{"children":["$L1","$0:props:children:0"]}]
1:"BAR"
`.trim(),
);
});
});
});
Expand Up @@ -231,7 +231,7 @@ describe('ReactFlightDOMEdge', () => {
const [stream1, stream2] = passThrough(stream).tee();

const serializedContent = await readResult(stream1);
expect(serializedContent.length).toBeLessThan(400);
expect(serializedContent.length).toBeLessThan(470);

const result = await ReactServerDOMClient.createFromReadableStream(
stream2,
Expand Down