From 6c3202b1e1a4382b91de4fdc3b9bc9ed7a77619b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 22 Mar 2021 16:10:57 -0400 Subject: [PATCH] [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 --- .../src/__tests__/ReactDOMFizzServer-test.js | 80 +++++++++++++++++++ .../src/server/ReactDOMFizzServerBrowser.js | 6 +- .../src/server/ReactDOMFizzServerNode.js | 4 + .../src/server/ReactDOMServerFormatConfig.js | 47 ++++++----- .../server/ReactNativeServerFormatConfig.js | 2 + .../src/ReactNoopServer.js | 16 ++-- packages/react-server/src/ReactFizzServer.js | 8 +- .../forks/ReactServerFormatConfig.custom.js | 1 - 8 files changed, 133 insertions(+), 31 deletions(-) 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 //