Skip to content

Commit 77db198

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 c49a32f commit 77db198

File tree

4 files changed

+64
-11
lines changed

4 files changed

+64
-11
lines changed

packages/react-server-dom-fb/src/ReactFlightReferencesFB.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
export type ClientManifest = null;
1111

1212
// eslint-disable-next-line no-unused-vars
13-
export type ServerReference<T> = string;
13+
export type ServerReference<T> = {};
1414

1515
// eslint-disable-next-line no-unused-vars
1616
export type ClientReference<T> = {

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: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,10 @@ export function createRequest(
325325
rootContext,
326326
abortSet,
327327
);
328+
// TODO
329+
if (model !== null && typeof model === 'object') {
330+
request.writtenObjects.set(model, rootTask.id);
331+
}
328332
pingedTasks.push(rootTask);
329333
return request;
330334
}
@@ -787,10 +791,6 @@ function createTask(
787791
abortSet: Set<Task>,
788792
): Task {
789793
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-
}
794794
const task: Task = {
795795
id,
796796
status: PENDING,
@@ -988,7 +988,17 @@ function serializeClientReference(
988988
}
989989
}
990990

991-
function outlineModel(request: Request, value: ReactClientValue): number {
991+
function outlineModel(
992+
request: Request,
993+
value:
994+
| ClientReference<any>
995+
| ServerReference<any>
996+
| Iterable<ReactClientValue>
997+
| Array<ReactClientValue>
998+
| Map<ReactClientValue, ReactClientValue>
999+
| Set<ReactClientValue>
1000+
| ReactClientObject,
1001+
): number {
9921002
request.pendingChunks++;
9931003
const newTask = createTask(
9941004
request,
@@ -998,6 +1008,7 @@ function outlineModel(request: Request, value: ReactClientValue): number {
9981008
rootContextSnapshot, // Therefore we don't pass any contextual information along.
9991009
request.abortableTasks,
10001010
);
1011+
request.writtenObjects.set(value, newTask.id);
10011012
retryTask(request, newTask);
10021013
return newTask.id;
10031014
}
@@ -1251,9 +1262,18 @@ function renderModelDestructive(
12511262
const writtenObjects = request.writtenObjects;
12521263
const existingId = writtenObjects.get(value);
12531264
if (existingId !== undefined) {
1254-
if (existingId === -1) {
1265+
if (
1266+
enableServerComponentKeys &&
1267+
(task.keyPath !== null ||
1268+
task.implicitSlot ||
1269+
task.context !== rootContextSnapshot)
1270+
) {
1271+
// If we're in some kind of context we can't reuse the result of this render or
1272+
// previous renders of this element. We only reuse elements if they're not wrapped
1273+
// by another Server Component.
1274+
} else if (existingId === -1) {
12551275
// Seen but not yet outlined.
1256-
const newId = outlineModel(request, value);
1276+
const newId = outlineModel(request, (value: any));
12571277
return serializeByValueID(newId);
12581278
} else if (modelRoot === value) {
12591279
// This is the ID we're currently emitting so we need to write it
@@ -1359,7 +1379,7 @@ function renderModelDestructive(
13591379
if (existingId !== undefined) {
13601380
if (existingId === -1) {
13611381
// Seen but not yet outlined.
1362-
const newId = outlineModel(request, value);
1382+
const newId = outlineModel(request, (value: any));
13631383
return serializeByValueID(newId);
13641384
} else if (modelRoot === value) {
13651385
// This is the ID we're currently emitting so we need to write it

packages/react-server/src/ReactFlightServerConfigBundlerCustom.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99

1010
declare var $$$config: any;
1111

12+
interface Reference {}
13+
1214
export opaque type ClientManifest = mixed;
13-
export opaque type ClientReference<T> = mixed; // eslint-disable-line no-unused-vars
14-
export opaque type ServerReference<T> = mixed; // eslint-disable-line no-unused-vars
15+
export opaque type ClientReference<T>: Reference = Reference; // eslint-disable-line no-unused-vars
16+
export opaque type ServerReference<T>: Reference = Reference; // eslint-disable-line no-unused-vars
1517
export opaque type ClientReferenceMetadata: any = mixed;
1618
export opaque type ServerReferenceId: any = mixed;
1719
export opaque type ClientReferenceKey: any = mixed;

0 commit comments

Comments
 (0)