Skip to content

Commit 18c8a4f

Browse files
committed
Don't dedupe Elements if they're in a non-default Context
If an element gets wrapped in a different server component then that has a different keyPath context and the element might end up with a different key. So we don't use the deduping mechanism if we're already inside a Server Component parent with a key or otherwise. Only the simple case gets deduped. The props of a client element are still deduped though if they're the same instance.
1 parent cdeec64 commit 18c8a4f

File tree

2 files changed

+55
-8
lines changed

2 files changed

+55
-8
lines changed

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,37 @@ describe('ReactFlightDOMEdge', () => {
225225
const stream = ReactServerDOMServer.renderToReadableStream(children);
226226
const [stream1, stream2] = passThrough(stream).tee();
227227

228+
const serializedContent = await readResult(stream1);
229+
230+
expect(serializedContent.length).toBeLessThan(400);
231+
expect(timesRendered).toBeLessThan(5);
232+
233+
const result = await ReactServerDOMClient.createFromReadableStream(
234+
stream2,
235+
{
236+
ssrManifest: {
237+
moduleMap: null,
238+
moduleLoading: null,
239+
},
240+
},
241+
);
242+
// Should still match the result when parsed
243+
expect(result).toEqual(resolvedChildren);
244+
});
245+
246+
it('should execute repeated host components only once', async () => {
247+
const div = <div>this is a long return value</div>;
248+
let timesRendered = 0;
249+
function ServerComponent() {
250+
timesRendered++;
251+
return div;
252+
}
253+
const element = <ServerComponent />;
254+
const children = new Array(30).fill(element);
255+
const resolvedChildren = new Array(30).fill(div);
256+
const stream = ReactServerDOMServer.renderToReadableStream(children);
257+
const [stream1, stream2] = passThrough(stream).tee();
258+
228259
const serializedContent = await readResult(stream1);
229260
expect(serializedContent.length).toBeLessThan(400);
230261
expect(timesRendered).toBeLessThan(5);

packages/react-server/src/ReactFlightServer.js

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -787,10 +787,6 @@ function createTask(
787787
abortSet: Set<Task>,
788788
): Task {
789789
const id = request.nextChunkId++;
790-
if (typeof model === 'object' && model !== null) {
791-
// Register this model as having the ID we're about to write.
792-
request.writtenObjects.set(model, id);
793-
}
794790
const task: Task = {
795791
id,
796792
status: PENDING,
@@ -988,7 +984,17 @@ function serializeClientReference(
988984
}
989985
}
990986

991-
function outlineModel(request: Request, value: ReactClientValue): number {
987+
function outlineModel(
988+
request: Request,
989+
value:
990+
| ClientReference<any>
991+
| ServerReference<any>
992+
| Iterable<ReactClientValue>
993+
| Array<ReactClientValue>
994+
| Map<ReactClientValue, ReactClientValue>
995+
| Set<ReactClientValue>
996+
| ReactClientObject,
997+
): number {
992998
request.pendingChunks++;
993999
const newTask = createTask(
9941000
request,
@@ -998,6 +1004,7 @@ function outlineModel(request: Request, value: ReactClientValue): number {
9981004
rootContextSnapshot, // Therefore we don't pass any contextual information along.
9991005
request.abortableTasks,
10001006
);
1007+
request.writtenObjects.set(value, newTask.id);
10011008
retryTask(request, newTask);
10021009
return newTask.id;
10031010
}
@@ -1251,9 +1258,18 @@ function renderModelDestructive(
12511258
const writtenObjects = request.writtenObjects;
12521259
const existingId = writtenObjects.get(value);
12531260
if (existingId !== undefined) {
1254-
if (existingId === -1) {
1261+
if (
1262+
enableServerComponentKeys &&
1263+
(task.keyPath !== null ||
1264+
task.implicitSlot ||
1265+
task.context !== rootContextSnapshot)
1266+
) {
1267+
// If we're in some kind of context we can't reuse the result of this render or
1268+
// previous renders of this element. We only reuse elements if they're not wrapped
1269+
// by another Server Component.
1270+
} else if (existingId === -1) {
12551271
// Seen but not yet outlined.
1256-
const newId = outlineModel(request, value);
1272+
const newId = outlineModel(request, (value: any));
12571273
return serializeByValueID(newId);
12581274
} else if (modelRoot === value) {
12591275
// This is the ID we're currently emitting so we need to write it
@@ -1360,7 +1376,7 @@ function renderModelDestructive(
13601376
if (existingId !== undefined) {
13611377
if (existingId === -1) {
13621378
// Seen but not yet outlined.
1363-
const newId = outlineModel(request, value);
1379+
const newId = outlineModel(request, (value: any));
13641380
return serializeByValueID(newId);
13651381
} else if (modelRoot === value) {
13661382
// This is the ID we're currently emitting so we need to write it

0 commit comments

Comments
 (0)