Skip to content

Commit 6c3202b

Browse files
authored
[Fizz] Use identifierPrefix to avoid conflicts within the same response (#21037)
* Use identifierPrefix to avoid conflicts within the same response identifierPrefix as an option exists to avoid useOpaqueIdentifier conflicting when different renders are used within one HTML response. This lets this be configured for the DOM renderer specifically since it's DOM specific whether they will conflict across trees or not. * Add test for using multiple containers in one HTML document
1 parent dcdf8de commit 6c3202b

File tree

8 files changed

+133
-31
lines changed

8 files changed

+133
-31
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,4 +320,84 @@ describe('ReactDOMFizzServer', () => {
320320
</div>,
321321
);
322322
});
323+
324+
// @gate experimental
325+
it('should allow for two containers to be written to the same document', async () => {
326+
// We create two passthrough streams for each container to write into.
327+
// Notably we don't implement a end() call for these. Because we don't want to
328+
// close the underlying stream just because one of the streams is done. Instead
329+
// we manually close when both are done.
330+
const writableA = new Stream.Writable();
331+
writableA._write = (chunk, encoding, next) => {
332+
writable.write(chunk, encoding, next);
333+
};
334+
const writableB = new Stream.Writable();
335+
writableB._write = (chunk, encoding, next) => {
336+
writable.write(chunk, encoding, next);
337+
};
338+
339+
writable.write('<div id="container-A">');
340+
await act(async () => {
341+
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
342+
<Suspense fallback={<Text text="Loading A..." />}>
343+
<Text text="This will show A: " />
344+
<div>
345+
<AsyncText text="A" />
346+
</div>
347+
</Suspense>,
348+
writableA,
349+
{identifierPrefix: 'A_'},
350+
);
351+
startWriting();
352+
});
353+
writable.write('</div>');
354+
355+
writable.write('<div id="container-B">');
356+
await act(async () => {
357+
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
358+
<Suspense fallback={<Text text="Loading B..." />}>
359+
<Text text="This will show B: " />
360+
<div>
361+
<AsyncText text="B" />
362+
</div>
363+
</Suspense>,
364+
writableB,
365+
{identifierPrefix: 'B_'},
366+
);
367+
startWriting();
368+
});
369+
writable.write('</div>');
370+
371+
expect(getVisibleChildren(container)).toEqual([
372+
<div id="container-A">Loading A...</div>,
373+
<div id="container-B">Loading B...</div>,
374+
]);
375+
376+
await act(async () => {
377+
resolveText('B');
378+
});
379+
380+
expect(getVisibleChildren(container)).toEqual([
381+
<div id="container-A">Loading A...</div>,
382+
<div id="container-B">
383+
This will show B: <div>B</div>
384+
</div>,
385+
]);
386+
387+
await act(async () => {
388+
resolveText('A');
389+
});
390+
391+
// We're done writing both streams now.
392+
writable.end();
393+
394+
expect(getVisibleChildren(container)).toEqual([
395+
<div id="container-A">
396+
This will show A: <div>A</div>
397+
</div>,
398+
<div id="container-B">
399+
This will show B: <div>B</div>
400+
</div>,
401+
]);
402+
});
323403
});

packages/react-dom/src/server/ReactDOMFizzServerBrowser.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ import {
1616
abort,
1717
} from 'react-server/src/ReactFizzServer';
1818

19+
import {createResponseState} from './ReactDOMServerFormatConfig';
20+
1921
type Options = {
20-
signal?: AbortSignal,
22+
identifierPrefix?: string,
2123
progressiveChunkSize?: number,
24+
signal?: AbortSignal,
2225
};
2326

2427
function renderToReadableStream(
@@ -39,6 +42,7 @@ function renderToReadableStream(
3942
request = createRequest(
4043
children,
4144
controller,
45+
createResponseState(options ? options.identifierPrefix : undefined),
4246
options ? options.progressiveChunkSize : undefined,
4347
);
4448
startWork(request);

packages/react-dom/src/server/ReactDOMFizzServerNode.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@ import {
1717
abort,
1818
} from 'react-server/src/ReactFizzServer';
1919

20+
import {createResponseState} from './ReactDOMServerFormatConfig';
21+
2022
function createDrainHandler(destination, request) {
2123
return () => startFlowing(request);
2224
}
2325

2426
type Options = {
27+
identifierPrefix?: string,
2528
progressiveChunkSize?: number,
2629
};
2730

@@ -39,6 +42,7 @@ function pipeToNodeWritable(
3942
const request = createRequest(
4043
children,
4144
destination,
45+
createResponseState(options ? options.identifierPrefix : undefined),
4246
options ? options.progressiveChunkSize : undefined,
4347
);
4448
let hasStartedFlowing = false;

packages/react-dom/src/server/ReactDOMServerFormatConfig.js

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,25 @@ import invariant from 'shared/invariant';
2424

2525
// Per response,
2626
export type ResponseState = {
27+
placeholderPrefix: PrecomputedChunk,
28+
segmentPrefix: PrecomputedChunk,
29+
boundaryPrefix: string,
30+
opaqueIdentifierPrefix: PrecomputedChunk,
2731
nextSuspenseID: number,
2832
sentCompleteSegmentFunction: boolean,
2933
sentCompleteBoundaryFunction: boolean,
3034
sentClientRenderFunction: boolean,
3135
};
3236

3337
// Allows us to keep track of what we've already written so we can refer back to it.
34-
export function createResponseState(): ResponseState {
38+
export function createResponseState(
39+
identifierPrefix: string = '',
40+
): ResponseState {
3541
return {
42+
placeholderPrefix: stringToPrecomputedChunk(identifierPrefix + 'P:'),
43+
segmentPrefix: stringToPrecomputedChunk(identifierPrefix + 'S:'),
44+
boundaryPrefix: identifierPrefix + 'B:',
45+
opaqueIdentifierPrefix: stringToPrecomputedChunk(identifierPrefix + 'R:'),
3646
nextSuspenseID: 0,
3747
sentCompleteSegmentFunction: false,
3848
sentCompleteBoundaryFunction: false,
@@ -68,7 +78,7 @@ function assignAnID(
6878
// TODO: This approach doesn't yield deterministic results since this is assigned during render.
6979
const generatedID = responseState.nextSuspenseID++;
7080
return (id.formattedID = stringToPrecomputedChunk(
71-
'B:' + generatedID.toString(16),
81+
responseState.boundaryPrefix + generatedID.toString(16),
7282
));
7383
}
7484

@@ -160,20 +170,19 @@ export function pushEndInstance(
160170
// A placeholder is a node inside a hidden partial tree that can be filled in later, but before
161171
// display. It's never visible to users.
162172
const placeholder1 = stringToPrecomputedChunk('<span id="');
163-
const placeholder2 = stringToPrecomputedChunk('P:');
164-
const placeholder3 = stringToPrecomputedChunk('"></span>');
173+
const placeholder2 = stringToPrecomputedChunk('"></span>');
165174
export function writePlaceholder(
166175
destination: Destination,
176+
responseState: ResponseState,
167177
id: number,
168178
): boolean {
169179
// TODO: This needs to be contextually aware and switch tag since not all parents allow for spans like
170180
// <select> or <tbody>. E.g. suspending a component that renders a table row.
171181
writeChunk(destination, placeholder1);
172-
// TODO: Use the identifierPrefix option to make the prefix configurable.
173-
writeChunk(destination, placeholder2);
182+
writeChunk(destination, responseState.placeholderPrefix);
174183
const formattedID = stringToChunk(id.toString(16));
175184
writeChunk(destination, formattedID);
176-
return writeChunk(destination, placeholder3);
185+
return writeChunk(destination, placeholder2);
177186
}
178187

179188
// Suspense boundaries are encoded as comments.
@@ -207,20 +216,19 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
207216
}
208217

209218
const startSegment = stringToPrecomputedChunk('<div hidden id="');
210-
const startSegment2 = stringToPrecomputedChunk('S:');
211-
const startSegment3 = stringToPrecomputedChunk('">');
219+
const startSegment2 = stringToPrecomputedChunk('">');
212220
const endSegment = stringToPrecomputedChunk('</div>');
213221
export function writeStartSegment(
214222
destination: Destination,
223+
responseState: ResponseState,
215224
id: number,
216225
): boolean {
217226
// TODO: What happens with special children like <tr> if they're inserted in a div? Maybe needs contextually aware containers.
218227
writeChunk(destination, startSegment);
219-
// TODO: Use the identifierPrefix option to make the prefix configurable.
220-
writeChunk(destination, startSegment2);
228+
writeChunk(destination, responseState.segmentPrefix);
221229
const formattedID = stringToChunk(id.toString(16));
222230
writeChunk(destination, formattedID);
223-
return writeChunk(destination, startSegment3);
231+
return writeChunk(destination, startSegment2);
224232
}
225233
export function writeEndSegment(destination: Destination): boolean {
226234
return writeChunk(destination, endSegment);
@@ -349,12 +357,10 @@ const clientRenderFunction =
349357
'function $RX(b){if(b=document.getElementById(b)){do b=b.previousSibling;while(8!==b.nodeType||"$?"!==b.data);b.data="$!";b._reactRetry&&b._reactRetry()}}';
350358

351359
const completeSegmentScript1Full = stringToPrecomputedChunk(
352-
'<script>' + completeSegmentFunction + ';$RS("S:',
353-
);
354-
const completeSegmentScript1Partial = stringToPrecomputedChunk(
355-
'<script>$RS("S:',
360+
'<script>' + completeSegmentFunction + ';$RS("',
356361
);
357-
const completeSegmentScript2 = stringToPrecomputedChunk('","P:');
362+
const completeSegmentScript1Partial = stringToPrecomputedChunk('<script>$RS("');
363+
const completeSegmentScript2 = stringToPrecomputedChunk('","');
358364
const completeSegmentScript3 = stringToPrecomputedChunk('")</script>');
359365

360366
export function writeCompletedSegmentInstruction(
@@ -370,10 +376,11 @@ export function writeCompletedSegmentInstruction(
370376
// Future calls can just reuse the same function.
371377
writeChunk(destination, completeSegmentScript1Partial);
372378
}
373-
// TODO: Use the identifierPrefix option to make the prefix configurable.
379+
writeChunk(destination, responseState.segmentPrefix);
374380
const formattedID = stringToChunk(contentSegmentID.toString(16));
375381
writeChunk(destination, formattedID);
376382
writeChunk(destination, completeSegmentScript2);
383+
writeChunk(destination, responseState.placeholderPrefix);
377384
writeChunk(destination, formattedID);
378385
return writeChunk(destination, completeSegmentScript3);
379386
}
@@ -384,7 +391,7 @@ const completeBoundaryScript1Full = stringToPrecomputedChunk(
384391
const completeBoundaryScript1Partial = stringToPrecomputedChunk(
385392
'<script>$RC("',
386393
);
387-
const completeBoundaryScript2 = stringToPrecomputedChunk('","S:');
394+
const completeBoundaryScript2 = stringToPrecomputedChunk('","');
388395
const completeBoundaryScript3 = stringToPrecomputedChunk('")</script>');
389396

390397
export function writeCompletedBoundaryInstruction(
@@ -401,7 +408,6 @@ export function writeCompletedBoundaryInstruction(
401408
// Future calls can just reuse the same function.
402409
writeChunk(destination, completeBoundaryScript1Partial);
403410
}
404-
// TODO: Use the identifierPrefix option to make the prefix configurable.
405411
const formattedBoundaryID = boundaryID.formattedID;
406412
invariant(
407413
formattedBoundaryID !== null,
@@ -410,6 +416,7 @@ export function writeCompletedBoundaryInstruction(
410416
const formattedContentID = stringToChunk(contentSegmentID.toString(16));
411417
writeChunk(destination, formattedBoundaryID);
412418
writeChunk(destination, completeBoundaryScript2);
419+
writeChunk(destination, responseState.segmentPrefix);
413420
writeChunk(destination, formattedContentID);
414421
return writeChunk(destination, completeBoundaryScript3);
415422
}

packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ function formatID(id: number): Uint8Array {
145145
// display. It's never visible to users.
146146
export function writePlaceholder(
147147
destination: Destination,
148+
responseState: ResponseState,
148149
id: number,
149150
): boolean {
150151
writeChunk(destination, PLACEHOLDER);
@@ -179,6 +180,7 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
179180

180181
export function writeStartSegment(
181182
destination: Destination,
183+
responseState: ResponseState,
182184
id: number,
183185
): boolean {
184186
writeChunk(destination, SEGMENT);

packages/react-noop-renderer/src/ReactNoopServer.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,6 @@ const ReactNoopServer = ReactFizzServer({
7777
closeWithError(destination: Destination, error: mixed): void {},
7878
flushBuffered(destination: Destination): void {},
7979

80-
createResponseState(): null {
81-
return null;
82-
},
8380
createSuspenseBoundaryID(): SuspenseInstance {
8481
// The ID is a pointer to the boundary itself.
8582
return {state: 'pending', children: []};
@@ -114,7 +111,11 @@ const ReactNoopServer = ReactFizzServer({
114111
target.push(POP);
115112
},
116113

117-
writePlaceholder(destination: Destination, id: number): boolean {
114+
writePlaceholder(
115+
destination: Destination,
116+
responseState: ResponseState,
117+
id: number,
118+
): boolean {
118119
const parent = destination.stack[destination.stack.length - 1];
119120
destination.placeholders.set(id, {
120121
parent: parent,
@@ -153,7 +154,11 @@ const ReactNoopServer = ReactFizzServer({
153154
destination.stack.pop();
154155
},
155156

156-
writeStartSegment(destination: Destination, id: number): boolean {
157+
writeStartSegment(
158+
destination: Destination,
159+
responseState: ResponseState,
160+
id: number,
161+
): boolean {
157162
const segment = {
158163
children: [],
159164
};
@@ -227,6 +232,7 @@ function render(children: React$Element<any>, options?: Options): Destination {
227232
const request = ReactNoopServer.createRequest(
228233
children,
229234
destination,
235+
null,
230236
options ? options.progressiveChunkSize : undefined,
231237
);
232238
ReactNoopServer.startWork(request);

packages/react-server/src/ReactFizzServer.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ import {
4444
pushStartInstance,
4545
pushEndInstance,
4646
createSuspenseBoundaryID,
47-
createResponseState,
4847
} from './ReactServerFormatConfig';
4948
import {REACT_ELEMENT_TYPE, REACT_SUSPENSE_TYPE} from 'shared/ReactSymbols';
5049
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -133,13 +132,14 @@ const DEFAULT_PROGRESSIVE_CHUNK_SIZE = 12800;
133132
export function createRequest(
134133
children: ReactNodeList,
135134
destination: Destination,
135+
responseState: ResponseState,
136136
progressiveChunkSize: number = DEFAULT_PROGRESSIVE_CHUNK_SIZE,
137137
): Request {
138138
const pingedWork = [];
139139
const abortSet: Set<SuspendedWork> = new Set();
140140
const request = {
141141
destination,
142-
responseState: createResponseState(),
142+
responseState,
143143
progressiveChunkSize,
144144
status: BUFFERING,
145145
nextSegmentId: 0,
@@ -590,7 +590,7 @@ function flushSubtree(
590590
// We're emitting a placeholder for this segment to be filled in later.
591591
// Therefore we'll need to assign it an ID - to refer to it by.
592592
const segmentID = (segment.id = request.nextSegmentId++);
593-
return writePlaceholder(destination, segmentID);
593+
return writePlaceholder(destination, request.responseState, segmentID);
594594
}
595595
case COMPLETED: {
596596
segment.status = FLUSHED;
@@ -712,7 +712,7 @@ function flushSegmentContainer(
712712
destination: Destination,
713713
segment: Segment,
714714
): boolean {
715-
writeStartSegment(destination, segment.id);
715+
writeStartSegment(destination, request.responseState, segment.id);
716716
flushSegment(request, destination, segment);
717717
return writeEndSegment(destination);
718718
}

packages/react-server/src/forks/ReactServerFormatConfig.custom.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ export opaque type Destination = mixed; // eslint-disable-line no-undef
2828
export opaque type ResponseState = mixed;
2929
export opaque type SuspenseBoundaryID = mixed;
3030

31-
export const createResponseState = $$$hostConfig.createResponseState;
3231
export const createSuspenseBoundaryID = $$$hostConfig.createSuspenseBoundaryID;
3332
export const pushEmpty = $$$hostConfig.pushEmpty;
3433
export const pushTextInstance = $$$hostConfig.pushTextInstance;

0 commit comments

Comments
 (0)