diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 9f139dd3baf1..3a7d7205dcb2 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...
, +
+ This will show B:
B
+
, + ]); + + await act(async () => { + resolveText('A'); + }); + + // We're done writing both streams now. + writable.end(); + + expect(getVisibleChildren(container)).toEqual([ +
+ This will show A:
A
+
, +
+ This will show B:
B
+
, + ]); + }); }); diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js index b310271291e2..902989fefca0 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 1bc2506927db..8de76f7095f9 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 250892b31d4a..e9d337724e19 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 //