Skip to content

Commit 6baff7a

Browse files
authored
[Flight] Allow cyclic references to be serialized when unwrapping lazy elements (facebook#35471)
When `renderModelDestructive` unwraps a lazy element and subsequently calls `renderModelDestructive` again with the resolved model, we should preserve the parent connection so that cyclic references can be serialized properly. This can occur in an advanced scenario where the result from the Flight Client is serialized again with the Flight Server, e.g. for slicing a precomputed payload into multiple parts. Note: The added test only fails when run with `--prod`. In dev mode, the component info outlining prevents the issue from occurring.
1 parent bef88f7 commit 6baff7a

File tree

2 files changed

+70
-2
lines changed

2 files changed

+70
-2
lines changed

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,74 @@ describe('ReactFlightDOMEdge', () => {
321321
expect(result).toEqual('<span>Client Component</span>');
322322
});
323323

324+
it('should resolve cyclic references in client component props after two rounds of serialization and deserialization', async () => {
325+
const ClientComponent = clientExports(function ClientComponent({data}) {
326+
return (
327+
<div>{data.self === data ? 'Cycle resolved' : 'Cycle broken'}</div>
328+
);
329+
});
330+
const clientModuleMetadata = webpackMap[ClientComponent.$$id];
331+
const consumerModuleId = 'consumer-' + clientModuleMetadata.id;
332+
const clientReference = Object.defineProperties(ClientComponent, {
333+
$$typeof: {value: Symbol.for('react.client.reference')},
334+
$$id: {value: ClientComponent.$$id},
335+
});
336+
webpackModules[consumerModuleId] = clientReference;
337+
338+
const cyclic = {self: null};
339+
cyclic.self = cyclic;
340+
341+
const stream1 = ReactServerDOMServer.renderToReadableStream(
342+
<React.Fragment key="this-key-is-important-to-repro-a-prior-cycle-serialization-bug">
343+
<ClientComponent data={cyclic} />
344+
</React.Fragment>,
345+
webpackMap,
346+
);
347+
348+
const promise = ReactServerDOMClient.createFromReadableStream(stream1, {
349+
serverConsumerManifest: {
350+
moduleMap: {
351+
[clientModuleMetadata.id]: {
352+
'*': {
353+
id: consumerModuleId,
354+
chunks: [],
355+
name: '*',
356+
},
357+
},
358+
},
359+
moduleLoading: webpackModuleLoading,
360+
serverModuleMap: null,
361+
},
362+
});
363+
364+
const errors = [];
365+
const stream2 = await serverAct(() =>
366+
ReactServerDOMServer.renderToReadableStream(promise, webpackMap, {
367+
onError(error) {
368+
errors.push(error);
369+
},
370+
}),
371+
);
372+
373+
expect(errors).toEqual([]);
374+
375+
const element = await serverAct(() =>
376+
ReactServerDOMClient.createFromReadableStream(stream2, {
377+
serverConsumerManifest: {
378+
moduleMap: null,
379+
moduleLoading: null,
380+
},
381+
}),
382+
);
383+
384+
const ssrStream = await serverAct(() =>
385+
ReactDOMServer.renderToReadableStream(element),
386+
);
387+
const result = await readResult(ssrStream);
388+
389+
expect(result).toBe('<div>Cycle resolved</div>');
390+
});
391+
324392
it('should be able to load a server reference on a consuming server if a mapping exists', async () => {
325393
function greet(name) {
326394
return 'hi, ' + name;

packages/react-server/src/ReactFlightServer.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3622,8 +3622,8 @@ function renderModelDestructive(
36223622
return renderModelDestructive(
36233623
request,
36243624
task,
3625-
emptyRoot,
3626-
'',
3625+
parent,
3626+
parentPropertyName,
36273627
resolvedModel,
36283628
);
36293629
}

0 commit comments

Comments
 (0)