diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
index 9f139dd3baf15..3a7d7205dcb27 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
@@ -320,4 +320,84 @@ describe('ReactDOMFizzServer', () => {
,
);
});
+
+ // @gate experimental
+ it('should allow for two containers to be written to the same document', async () => {
+ // We create two passthrough streams for each container to write into.
+ // Notably we don't implement a end() call for these. Because we don't want to
+ // close the underlying stream just because one of the streams is done. Instead
+ // we manually close when both are done.
+ const writableA = new Stream.Writable();
+ writableA._write = (chunk, encoding, next) => {
+ writable.write(chunk, encoding, next);
+ };
+ const writableB = new Stream.Writable();
+ writableB._write = (chunk, encoding, next) => {
+ writable.write(chunk, encoding, next);
+ };
+
+ writable.write('
');
+ await act(async () => {
+ const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
+
}>
+
+
+ ,
+ writableA,
+ {identifierPrefix: 'A_'},
+ );
+ startWriting();
+ });
+ writable.write('
');
+
+ writable.write('');
+ await act(async () => {
+ const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
+
}>
+
+
+ ,
+ writableB,
+ {identifierPrefix: 'B_'},
+ );
+ startWriting();
+ });
+ writable.write('
');
+
+ expect(getVisibleChildren(container)).toEqual([
+ Loading A...
,
+ Loading B...
,
+ ]);
+
+ await act(async () => {
+ resolveText('B');
+ });
+
+ expect(getVisibleChildren(container)).toEqual([
+ Loading A...
,
+ ,
+ ]);
+
+ await act(async () => {
+ resolveText('A');
+ });
+
+ // We're done writing both streams now.
+ writable.end();
+
+ expect(getVisibleChildren(container)).toEqual([
+ ,
+ ,
+ ]);
+ });
});
diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
index b310271291e2e..902989fefca0f 100644
--- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
+++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
@@ -16,9 +16,12 @@ import {
abort,
} from 'react-server/src/ReactFizzServer';
+import {createResponseState} from './ReactDOMServerFormatConfig';
+
type Options = {
- signal?: AbortSignal,
+ identifierPrefix?: string,
progressiveChunkSize?: number,
+ signal?: AbortSignal,
};
function renderToReadableStream(
@@ -39,6 +42,7 @@ function renderToReadableStream(
request = createRequest(
children,
controller,
+ createResponseState(options ? options.identifierPrefix : undefined),
options ? options.progressiveChunkSize : undefined,
);
startWork(request);
diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js
index 1bc2506927dbb..8de76f7095f9d 100644
--- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js
+++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js
@@ -17,11 +17,14 @@ import {
abort,
} from 'react-server/src/ReactFizzServer';
+import {createResponseState} from './ReactDOMServerFormatConfig';
+
function createDrainHandler(destination, request) {
return () => startFlowing(request);
}
type Options = {
+ identifierPrefix?: string,
progressiveChunkSize?: number,
};
@@ -39,6 +42,7 @@ function pipeToNodeWritable(
const request = createRequest(
children,
destination,
+ createResponseState(options ? options.identifierPrefix : undefined),
options ? options.progressiveChunkSize : undefined,
);
let hasStartedFlowing = false;
diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
index 250892b31d4a0..e9d337724e194 100644
--- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
@@ -24,6 +24,10 @@ import invariant from 'shared/invariant';
// Per response,
export type ResponseState = {
+ placeholderPrefix: PrecomputedChunk,
+ segmentPrefix: PrecomputedChunk,
+ boundaryPrefix: string,
+ opaqueIdentifierPrefix: PrecomputedChunk,
nextSuspenseID: number,
sentCompleteSegmentFunction: boolean,
sentCompleteBoundaryFunction: boolean,
@@ -31,8 +35,14 @@ export type ResponseState = {
};
// Allows us to keep track of what we've already written so we can refer back to it.
-export function createResponseState(): ResponseState {
+export function createResponseState(
+ identifierPrefix: string = '',
+): ResponseState {
return {
+ placeholderPrefix: stringToPrecomputedChunk(identifierPrefix + 'P:'),
+ segmentPrefix: stringToPrecomputedChunk(identifierPrefix + 'S:'),
+ boundaryPrefix: identifierPrefix + 'B:',
+ opaqueIdentifierPrefix: stringToPrecomputedChunk(identifierPrefix + 'R:'),
nextSuspenseID: 0,
sentCompleteSegmentFunction: false,
sentCompleteBoundaryFunction: false,
@@ -68,7 +78,7 @@ function assignAnID(
// TODO: This approach doesn't yield deterministic results since this is assigned during render.
const generatedID = responseState.nextSuspenseID++;
return (id.formattedID = stringToPrecomputedChunk(
- 'B:' + generatedID.toString(16),
+ responseState.boundaryPrefix + generatedID.toString(16),
));
}
@@ -160,20 +170,19 @@ export function pushEndInstance(
// A placeholder is a node inside a hidden partial tree that can be filled in later, but before
// display. It's never visible to users.
const placeholder1 = stringToPrecomputedChunk(' ');
+const placeholder2 = stringToPrecomputedChunk('">');
export function writePlaceholder(
destination: Destination,
+ responseState: ResponseState,
id: number,
): boolean {
// TODO: This needs to be contextually aware and switch tag since not all parents allow for spans like
// or . E.g. suspending a component that renders a table row.
writeChunk(destination, placeholder1);
- // TODO: Use the identifierPrefix option to make the prefix configurable.
- writeChunk(destination, placeholder2);
+ writeChunk(destination, responseState.placeholderPrefix);
const formattedID = stringToChunk(id.toString(16));
writeChunk(destination, formattedID);
- return writeChunk(destination, placeholder3);
+ return writeChunk(destination, placeholder2);
}
// Suspense boundaries are encoded as comments.
@@ -207,20 +216,19 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
}
const startSegment = stringToPrecomputedChunk('');
+const startSegment2 = stringToPrecomputedChunk('">');
const endSegment = stringToPrecomputedChunk('
');
export function writeStartSegment(
destination: Destination,
+ responseState: ResponseState,
id: number,
): boolean {
// TODO: What happens with special children like if they're inserted in a div? Maybe needs contextually aware containers.
writeChunk(destination, startSegment);
- // TODO: Use the identifierPrefix option to make the prefix configurable.
- writeChunk(destination, startSegment2);
+ writeChunk(destination, responseState.segmentPrefix);
const formattedID = stringToChunk(id.toString(16));
writeChunk(destination, formattedID);
- return writeChunk(destination, startSegment3);
+ return writeChunk(destination, startSegment2);
}
export function writeEndSegment(destination: Destination): boolean {
return writeChunk(destination, endSegment);
@@ -349,12 +357,10 @@ const clientRenderFunction =
'function $RX(b){if(b=document.getElementById(b)){do b=b.previousSibling;while(8!==b.nodeType||"$?"!==b.data);b.data="$!";b._reactRetry&&b._reactRetry()}}';
const completeSegmentScript1Full = stringToPrecomputedChunk(
- '');
export function writeCompletedSegmentInstruction(
@@ -370,10 +376,11 @@ export function writeCompletedSegmentInstruction(
// Future calls can just reuse the same function.
writeChunk(destination, completeSegmentScript1Partial);
}
- // TODO: Use the identifierPrefix option to make the prefix configurable.
+ writeChunk(destination, responseState.segmentPrefix);
const formattedID = stringToChunk(contentSegmentID.toString(16));
writeChunk(destination, formattedID);
writeChunk(destination, completeSegmentScript2);
+ writeChunk(destination, responseState.placeholderPrefix);
writeChunk(destination, formattedID);
return writeChunk(destination, completeSegmentScript3);
}
@@ -384,7 +391,7 @@ const completeBoundaryScript1Full = stringToPrecomputedChunk(
const completeBoundaryScript1Partial = stringToPrecomputedChunk(
'');
export function writeCompletedBoundaryInstruction(
@@ -401,7 +408,6 @@ export function writeCompletedBoundaryInstruction(
// Future calls can just reuse the same function.
writeChunk(destination, completeBoundaryScript1Partial);
}
- // TODO: Use the identifierPrefix option to make the prefix configurable.
const formattedBoundaryID = boundaryID.formattedID;
invariant(
formattedBoundaryID !== null,
@@ -410,6 +416,7 @@ export function writeCompletedBoundaryInstruction(
const formattedContentID = stringToChunk(contentSegmentID.toString(16));
writeChunk(destination, formattedBoundaryID);
writeChunk(destination, completeBoundaryScript2);
+ writeChunk(destination, responseState.segmentPrefix);
writeChunk(destination, formattedContentID);
return writeChunk(destination, completeBoundaryScript3);
}
diff --git a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
index e8f6b9e14afff..d89fafea818e3 100644
--- a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+++ b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
@@ -145,6 +145,7 @@ function formatID(id: number): Uint8Array {
// display. It's never visible to users.
export function writePlaceholder(
destination: Destination,
+ responseState: ResponseState,
id: number,
): boolean {
writeChunk(destination, PLACEHOLDER);
@@ -179,6 +180,7 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
export function writeStartSegment(
destination: Destination,
+ responseState: ResponseState,
id: number,
): boolean {
writeChunk(destination, SEGMENT);
diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js
index 99392ebed6db5..d2a4bf032933c 100644
--- a/packages/react-noop-renderer/src/ReactNoopServer.js
+++ b/packages/react-noop-renderer/src/ReactNoopServer.js
@@ -77,9 +77,6 @@ const ReactNoopServer = ReactFizzServer({
closeWithError(destination: Destination, error: mixed): void {},
flushBuffered(destination: Destination): void {},
- createResponseState(): null {
- return null;
- },
createSuspenseBoundaryID(): SuspenseInstance {
// The ID is a pointer to the boundary itself.
return {state: 'pending', children: []};
@@ -114,7 +111,11 @@ const ReactNoopServer = ReactFizzServer({
target.push(POP);
},
- writePlaceholder(destination: Destination, id: number): boolean {
+ writePlaceholder(
+ destination: Destination,
+ responseState: ResponseState,
+ id: number,
+ ): boolean {
const parent = destination.stack[destination.stack.length - 1];
destination.placeholders.set(id, {
parent: parent,
@@ -153,7 +154,11 @@ const ReactNoopServer = ReactFizzServer({
destination.stack.pop();
},
- writeStartSegment(destination: Destination, id: number): boolean {
+ writeStartSegment(
+ destination: Destination,
+ responseState: ResponseState,
+ id: number,
+ ): boolean {
const segment = {
children: [],
};
@@ -227,6 +232,7 @@ function render(children: React$Element, options?: Options): Destination {
const request = ReactNoopServer.createRequest(
children,
destination,
+ null,
options ? options.progressiveChunkSize : undefined,
);
ReactNoopServer.startWork(request);
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index 54049da40b39f..265bfb89c24f4 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -44,7 +44,6 @@ import {
pushStartInstance,
pushEndInstance,
createSuspenseBoundaryID,
- createResponseState,
} from './ReactServerFormatConfig';
import {REACT_ELEMENT_TYPE, REACT_SUSPENSE_TYPE} from 'shared/ReactSymbols';
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -133,13 +132,14 @@ const DEFAULT_PROGRESSIVE_CHUNK_SIZE = 12800;
export function createRequest(
children: ReactNodeList,
destination: Destination,
+ responseState: ResponseState,
progressiveChunkSize: number = DEFAULT_PROGRESSIVE_CHUNK_SIZE,
): Request {
const pingedWork = [];
const abortSet: Set = new Set();
const request = {
destination,
- responseState: createResponseState(),
+ responseState,
progressiveChunkSize,
status: BUFFERING,
nextSegmentId: 0,
@@ -590,7 +590,7 @@ function flushSubtree(
// We're emitting a placeholder for this segment to be filled in later.
// Therefore we'll need to assign it an ID - to refer to it by.
const segmentID = (segment.id = request.nextSegmentId++);
- return writePlaceholder(destination, segmentID);
+ return writePlaceholder(destination, request.responseState, segmentID);
}
case COMPLETED: {
segment.status = FLUSHED;
@@ -712,7 +712,7 @@ function flushSegmentContainer(
destination: Destination,
segment: Segment,
): boolean {
- writeStartSegment(destination, segment.id);
+ writeStartSegment(destination, request.responseState, segment.id);
flushSegment(request, destination, segment);
return writeEndSegment(destination);
}
diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js
index 3f3688ad4fb88..76219e1a0748e 100644
--- a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js
+++ b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js
@@ -28,7 +28,6 @@ export opaque type Destination = mixed; // eslint-disable-line no-undef
export opaque type ResponseState = mixed;
export opaque type SuspenseBoundaryID = mixed;
-export const createResponseState = $$$hostConfig.createResponseState;
export const createSuspenseBoundaryID = $$$hostConfig.createSuspenseBoundaryID;
export const pushEmpty = $$$hostConfig.pushEmpty;
export const pushTextInstance = $$$hostConfig.pushTextInstance;