From 366eca0a72fe2b6af6c8085a4f2f25cce3bf74de Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 26 Mar 2021 11:58:12 -0400 Subject: [PATCH 1/6] Encode tables as a special insertion mode The table modes are special in that its children can't be created outside a table context so we need the segment container to be wrapped in a table. --- .../src/server/ReactDOMServerFormatConfig.js | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js index f7ed7ccf93971..060a21c7c9db7 100644 --- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js @@ -50,33 +50,38 @@ export function createResponseState( }; } -// Constants for the namespace we use. We don't actually provide the namespace but conditionally -// use different segment parents based on namespace. Therefore we use constants instead of the string. -const ROOT_NAMESPACE = 0; // At the root we don't need to know which namespace it is. We just need to know that it's already the right one. -const HTML_NAMESPACE = 1; -const SVG_NAMESPACE = 2; -const MATHML_NAMESPACE = 3; - -type NamespaceFlag = 0 | 1 | 2 | 3; +// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion +// modes. We only include the variants as they matter for the sake of our purposes. +// We don't actually provide the namespace therefore we use constants instead of the string. +const ROOT_MODE = 0; // At the root we don't need to know which mode it is. We just need to know that it's already the right one. +const SVG_MODE = 1; +const MATHML_MODE = 2; +const HTML_MODE = 3; // If we reenter HTML from SVG we know for sure it's HTML. +const HTML_TABLE_MODE = 4; +const HTML_TABLE_BODY_MODE = 5; +const HTML_TABLE_ROW_MODE = 6; +const HTML_COLGROUP_MODE = 7; + +type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; // Lets us keep track of contextual state and pick it back up after suspending. export type FormatContext = { - namespace: NamespaceFlag, // root/svg/html/mathml + insertionMode: InsertionMode, // root/svg/html/mathml/table selectedValue: null | string, // the selected value(s) inside a }; function createFormatContext( - namespace: NamespaceFlag, + insertionMode: InsertionMode, selectedValue: null | string, ): FormatContext { return { - namespace, + insertionMode, selectedValue, }; } export function createRootFormatContext(): FormatContext { - return createFormatContext(ROOT_NAMESPACE, null); + return createFormatContext(ROOT_MODE, null); } export function getChildFormatContext( @@ -87,15 +92,32 @@ export function getChildFormatContext( switch (type) { case 'select': return createFormatContext( - parentContext.namespace, + HTML_MODE, props.value != null ? props.value : props.defaultValue, ); case 'svg': - return createFormatContext(SVG_NAMESPACE, null); + return createFormatContext(SVG_MODE, null); case 'math': - return createFormatContext(MATHML_NAMESPACE, null); + return createFormatContext(MATHML_MODE, null); case 'foreignObject': - return createFormatContext(HTML_NAMESPACE, null); + return createFormatContext(HTML_MODE, null); + // Table parents are special in that their children can only be created at all if they're + // wrapped in a table parent. So we need to encode that we're entering this mode. + case 'table': + return createFormatContext(HTML_TABLE_MODE, null); + case 'thead': + case 'tbody': + case 'tfoot': + return createFormatContext(HTML_TABLE_BODY_MODE, null); + case 'colgroup': + return createFormatContext(HTML_COLGROUP_MODE, null); + case 'tr': + return createFormatContext(HTML_TABLE_ROW_MODE, null); + } + if (parentContext.insertionMode >= HTML_TABLE_MODE) { + // Whatever tag this was, it wasn't a table parent or other special parent, so we must have + // entered plain HTML again. + return createFormatContext(HTML_MODE, null); } return parentContext; } From c8b7a94e5ed0c3b141f4cc8fddcf649c5b1430b5 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 26 Mar 2021 13:53:19 -0400 Subject: [PATCH 2/6] Move formatContext from Task to Segment It works the same otherwise. It's just that this context needs to outlive the task so that I can use it when writing the segment. --- packages/react-server/src/ReactFizzServer.js | 38 ++++++++++++-------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index c4b795d24ff17..acc67775f816b 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -71,7 +71,6 @@ type Task = { blockedBoundary: Root | SuspenseBoundary, blockedSegment: Segment, // the segment we'll write to abortSet: Set, // the abortable set that this task belongs to - formatContext: FormatContext, assignID: null | SuspenseBoundaryID, // id to assign to the content }; @@ -90,6 +89,8 @@ type Segment = { +index: number, // the index within the parent's chunks or 0 at the root +chunks: Array, +children: Array, + // The context that this segment was created in. + formatContext: FormatContext, // If this segment represents a fallback, this is the content that will replace that fallback. +boundary: null | SuspenseBoundary, }; @@ -172,7 +173,7 @@ export function createRequest( onReadyToStream, }; // This segment represents the root fallback. - const rootSegment = createPendingSegment(request, 0, null); + const rootSegment = createPendingSegment(request, 0, null, rootContext); // There is no parent so conceptually, we're unblocked to flush this segment. rootSegment.parentFlushed = true; const rootTask = createTask( @@ -181,7 +182,6 @@ export function createRequest( null, rootSegment, abortSet, - rootContext, null, ); pingedTasks.push(rootTask); @@ -218,7 +218,6 @@ function createTask( blockedBoundary: Root | SuspenseBoundary, blockedSegment: Segment, abortSet: Set, - formatContext: FormatContext, assignID: null | SuspenseBoundaryID, ): Task { request.allPendingTasks++; @@ -233,7 +232,6 @@ function createTask( blockedBoundary, blockedSegment, abortSet, - formatContext, assignID, }; abortSet.add(task); @@ -244,6 +242,7 @@ function createPendingSegment( request: Request, index: number, boundary: null | SuspenseBoundary, + formatContext: FormatContext, ): Segment { return { status: PENDING, @@ -252,6 +251,7 @@ function createPendingSegment( parentFlushed: false, chunks: [], children: [], + formatContext, boundary, }; } @@ -317,7 +317,12 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void { // Something suspended, we'll need to create a new segment and resolve it later. const segment = task.blockedSegment; const insertionIndex = segment.chunks.length; - const newSegment = createPendingSegment(request, insertionIndex, null); + const newSegment = createPendingSegment( + request, + insertionIndex, + null, + segment.formatContext, + ); segment.children.push(newSegment); const newTask = createTask( request, @@ -325,7 +330,6 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void { task.blockedBoundary, newSegment, task.abortSet, - task.formatContext, task.assignID, ); // We've delegated the assignment. @@ -338,8 +342,9 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void { } } } else if (typeof type === 'string') { + const segment = task.blockedSegment; pushStartInstance( - task.blockedSegment.chunks, + segment.chunks, type, props, request.responseState, @@ -347,13 +352,13 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void { ); // We must have assigned it already above so we don't need this anymore. task.assignID = null; - const prevContext = task.formatContext; - task.formatContext = getChildFormatContext(prevContext, type, props); + const prevContext = segment.formatContext; + segment.formatContext = getChildFormatContext(prevContext, type, props); renderNode(request, task, props.children); // We expect that errors will fatal the whole task and that we don't need // the correct context. Therefore this is not in a finally. - task.formatContext = prevContext; - pushEndInstance(task.blockedSegment.chunks, type, props); + segment.formatContext = prevContext; + pushEndInstance(segment.chunks, type, props); } else if (type === REACT_SUSPENSE_TYPE) { const parentBoundary = task.blockedBoundary; const parentSegment = task.blockedSegment; @@ -376,11 +381,17 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void { request, insertionIndex, newBoundary, + parentSegment.formatContext, ); parentSegment.children.push(boundarySegment); // This segment is the actual child content. We can start rendering that immediately. - const contentRootSegment = createPendingSegment(request, 0, null); + const contentRootSegment = createPendingSegment( + request, + 0, + null, + parentSegment.formatContext, + ); // We mark the root segment as having its parent flushed. It's not really flushed but there is // no parent segment so there's nothing to wait on. contentRootSegment.parentFlushed = true; @@ -425,7 +436,6 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void { parentBoundary, boundarySegment, fallbackAbortSet, - task.formatContext, newBoundary.id, // This is the ID we want to give this fallback so we can replace it later. ); // TODO: This should be queued at a separate lower priority queue so that we only task From e7481041df2de88849d273d5db21b3bbe6ba6d1e Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 25 Mar 2021 10:10:34 -0400 Subject: [PATCH 3/6] Use template tag for placeholders and inserted dummy nodes with IDs These can be used in any parent. At least outside IE11. Not sure yet what happens in IE11 to these. Not sure if these are bad for perf since they're special nodes. --- .../src/__tests__/ReactDOMFizzServer-test.js | 71 ++++++++++++++++++- .../src/server/ReactDOMServerFormatConfig.js | 13 ++-- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 9021c23c6ce74..a6098574e5afc 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -97,7 +97,11 @@ describe('ReactDOMFizzServer', () => { let node = element.firstChild; while (node) { if (node.nodeType === 1) { - if (node.tagName !== 'SCRIPT' && !node.hasAttribute('hidden')) { + if ( + node.tagName !== 'SCRIPT' && + node.tagName !== 'TEMPLATE' && + !node.hasAttribute('hidden') + ) { const props = {}; const attributes = node.attributes; for (let i = 0; i < attributes.length; i++) { @@ -408,4 +412,69 @@ describe('ReactDOMFizzServer', () => { , ]); }); + + // @gate experimental + it('can resolve async content in esoteric parents', async () => { + function AsyncOption({text}) { + return ; + } + + function AsyncCol({className}) { + return {[]}; + } + + function App() { + return ( +
+ + + + + + +
+
+
+ ); + } + + await act(async () => { + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable( + , + writable, + ); + startWriting(); + }); + + expect(getVisibleChildren(container)).toEqual( +
+ Loading... +
, + ); + + await act(async () => { + resolveText('Hello'); + }); + + await act(async () => { + resolveText('World'); + }); + + expect(getVisibleChildren(container)).toEqual( +
+ + + + + +
+
, + ); + }); }); diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js index 060a21c7c9db7..84ed5b109f576 100644 --- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js @@ -154,8 +154,8 @@ function assignAnID( )); } -const dummyNode1 = stringToPrecomputedChunk(''); +const dummyNode1 = stringToPrecomputedChunk(''); function pushDummyNodeWithID( target: Array, @@ -247,16 +247,15 @@ export function pushEndInstance( // Structural Nodes // 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(''); +// display. It's never visible to users. We use the template tag because it can be used in every +// type of parent.