@samitouri / QOS-React / commits / 32d6f39edd

[Fizz] Support special HTML/SVG/MathML tags to suspend (#21113)

* 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. * 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. * 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. * Add special wrappers around inserted segments depending on their insertion mode * Allow the root namespace to be configured This allows us to insert the correct wrappers when streaming into an existing non-HTML tree. * Add comment

Sebastian Markbåge committed Mar 27, 2021 at 13:50 UTC 32d6f39eddd6d6b32b6c68068e96c66109ae8425
8 files changed +475 -59
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+247 -2
@@ -97,10 +97,22 @@ describe('ReactDOMFizzServer', () => {
97 let node = element.firstChild;
98 while (node) {
99 if (node.nodeType === 1) {
100 - if (node.tagName !== 'SCRIPT' && !node.hasAttribute('hidden')) {
100 + if (
101 + node.tagName !== 'SCRIPT' &&
102 + node.tagName !== 'TEMPLATE' &&
103 + !node.hasAttribute('hidden') &&
104 + !node.hasAttribute('aria-hidden')
105 + ) {
106 const props = {};
107 const attributes = node.attributes;
108 for (let i = 0; i < attributes.length; i++) {
109 + if (
110 + attributes[i].name === 'id' &&
111 + attributes[i].value.includes(':')
112 + ) {
113 + // We assume this is a React added ID that's a non-visual implementation detail.
114 + continue;
115 + }
116 props[attributes[i].name] = attributes[i].value;
117 }
118 props.children = getVisibleChildren(node);
@@ -112,7 +124,7 @@ describe('ReactDOMFizzServer', () => {
124 node = node.nextSibling;
125 }
126 return children.length === 0
115 - ? null
127 + ? undefined
128 : children.length === 1
129 ? children[0]
130 : children;
@@ -408,4 +420,237 @@ describe('ReactDOMFizzServer', () => {
420 </div>,
421 ]);
422 });
423 +
424 + // @gate experimental
425 + it('can resolve async content in esoteric parents', async () => {
426 + function AsyncOption({text}) {
427 + return <option>{readText(text)}</option>;
428 + }
429 +
430 + function AsyncCol({className}) {
431 + return <col className={readText(className)}>{[]}</col>;
432 + }
433 +
434 + function AsyncPath({id}) {
435 + return <path id={readText(id)}>{[]}</path>;
436 + }
437 +
438 + function AsyncMi({id}) {
439 + return <mi id={readText(id)}>{[]}</mi>;
440 + }
441 +
442 + function App() {
443 + return (
444 + <div>
445 + <select>
446 + <Suspense fallback="Loading...">
447 + <AsyncOption text="Hello" />
448 + </Suspense>
449 + </select>
450 + <Suspense fallback="Loading...">
451 + <table>
452 + <colgroup>
453 + <AsyncCol className="World" />
454 + </colgroup>
455 + </table>
456 + <svg>
457 + <g>
458 + <AsyncPath id="my-path" />
459 + </g>
460 + </svg>
461 + <math>
462 + <AsyncMi id="my-mi" />
463 + </math>
464 + </Suspense>
465 + </div>
466 + );
467 + }
468 +
469 + await act(async () => {
470 + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
471 + <App />,
472 + writable,
473 + );
474 + startWriting();
475 + });
476 +
477 + expect(getVisibleChildren(container)).toEqual(
478 + <div>
479 + <select>Loading...</select>Loading...
480 + </div>,
481 + );
482 +
483 + await act(async () => {
484 + resolveText('Hello');
485 + });
486 +
487 + await act(async () => {
488 + resolveText('World');
489 + });
490 +
491 + await act(async () => {
492 + resolveText('my-path');
493 + resolveText('my-mi');
494 + });
495 +
496 + expect(getVisibleChildren(container)).toEqual(
497 + <div>
498 + <select>
499 + <option>Hello</option>
500 + </select>
501 + <table>
502 + <colgroup>
503 + <col class="World" />
504 + </colgroup>
505 + </table>
506 + <svg>
507 + <g>
508 + <path id="my-path" />
509 + </g>
510 + </svg>
511 + <math>
512 + <mi id="my-mi" />
513 + </math>
514 + </div>,
515 + );
516 +
517 + expect(container.querySelector('#my-path').namespaceURI).toBe(
518 + 'http://www.w3.org/2000/svg',
519 + );
520 + expect(container.querySelector('#my-mi').namespaceURI).toBe(
521 + 'http://www.w3.org/1998/Math/MathML',
522 + );
523 + });
524 +
525 + // @gate experimental
526 + it('can resolve async content in table parents', async () => {
527 + function AsyncTableBody({className, children}) {
528 + return <tbody className={readText(className)}>{children}</tbody>;
529 + }
530 +
531 + function AsyncTableRow({className, children}) {
532 + return <tr className={readText(className)}>{children}</tr>;
533 + }
534 +
535 + function AsyncTableCell({text}) {
536 + return <td>{readText(text)}</td>;
537 + }
538 +
539 + function App() {
540 + return (
541 + <table>
542 + <Suspense
543 + fallback={
544 + <tbody>
545 + <tr>
546 + <td>Loading...</td>
547 + </tr>
548 + </tbody>
549 + }>
550 + <AsyncTableBody className="A">
551 + <AsyncTableRow className="B">
552 + <AsyncTableCell text="C" />
553 + </AsyncTableRow>
554 + </AsyncTableBody>
555 + </Suspense>
556 + </table>
557 + );
558 + }
559 +
560 + await act(async () => {
561 + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
562 + <App />,
563 + writable,
564 + );
565 + startWriting();
566 + });
567 +
568 + expect(getVisibleChildren(container)).toEqual(
569 + <table>
570 + <tbody>
571 + <tr>
572 + <td>Loading...</td>
573 + </tr>
574 + </tbody>
575 + </table>,
576 + );
577 +
578 + await act(async () => {
579 + resolveText('A');
580 + });
581 +
582 + await act(async () => {
583 + resolveText('B');
584 + });
585 +
586 + await act(async () => {
587 + resolveText('C');
588 + });
589 +
590 + expect(getVisibleChildren(container)).toEqual(
591 + <table>
592 + <tbody class="A">
593 + <tr class="B">
594 + <td>C</td>
595 + </tr>
596 + </tbody>
597 + </table>,
598 + );
599 + });
600 +
601 + // @gate experimental
602 + it('can stream into an SVG container', async () => {
603 + function AsyncPath({id}) {
604 + return <path id={readText(id)}>{[]}</path>;
605 + }
606 +
607 + function App() {
608 + return (
609 + <g>
610 + <Suspense fallback={<text>Loading...</text>}>
611 + <AsyncPath id="my-path" />
612 + </Suspense>
613 + </g>
614 + );
615 + }
616 +
617 + await act(async () => {
618 + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
619 + <App />,
620 + writable,
621 + {
622 + namespaceURI: 'http://www.w3.org/2000/svg',
623 + onReadyToStream() {
624 + writable.write('<svg>');
625 + startWriting();
626 + writable.write('</svg>');
627 + },
628 + },
629 + );
630 + });
631 +
632 + expect(getVisibleChildren(container)).toEqual(
633 + <svg>
634 + <g>
635 + <text>Loading...</text>
636 + </g>
637 + </svg>,
638 + );
639 +
640 + await act(async () => {
641 + resolveText('my-path');
642 + });
643 +
644 + expect(getVisibleChildren(container)).toEqual(
645 + <svg>
646 + <g>
647 + <path id="my-path" />
648 + </g>
649 + </svg>,
650 + );
651 +
652 + expect(container.querySelector('#my-path').namespaceURI).toBe(
653 + 'http://www.w3.org/2000/svg',
654 + );
655 + });
656 });
packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
+2 -1
@@ -23,6 +23,7 @@ import {
23
24 type Options = {
25 identifierPrefix?: string,
26 + namespaceURI?: string,
27 progressiveChunkSize?: number,
28 signal?: AbortSignal,
29 onReadyToStream?: () => void,
@@ -49,7 +50,7 @@ function renderToReadableStream(
50 children,
51 controller,
52 createResponseState(options ? options.identifierPrefix : undefined),
52 - createRootFormatContext(), // We call this here in case we need options to initialize it.
53 + createRootFormatContext(options ? options.namespaceURI : undefined),
54 options ? options.progressiveChunkSize : undefined,
55 options ? options.onError : undefined,
56 options ? options.onCompleteAll : undefined,
packages/react-dom/src/server/ReactDOMFizzServerNode.js
+2 -1
@@ -28,6 +28,7 @@ function createDrainHandler(destination, request) {
28
29 type Options = {
30 identifierPrefix?: string,
31 + namespaceURI?: string,
32 progressiveChunkSize?: number,
33 onReadyToStream?: () => void,
34 onCompleteAll?: () => void,
@@ -49,7 +50,7 @@ function pipeToNodeWritable(
50 children,
51 destination,
52 createResponseState(options ? options.identifierPrefix : undefined),
52 - createRootFormatContext(), // We call this here in case we need options to initialize it.
53 + createRootFormatContext(options ? options.namespaceURI : undefined),
54 options ? options.progressiveChunkSize : undefined,
55 options ? options.onError : undefined,
56 options ? options.onCompleteAll : undefined,
packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+184 -36
@@ -50,33 +50,45 @@ export function createResponseState(
50 };
51 }
52
53 -// Constants for the namespace we use. We don't actually provide the namespace but conditionally
54 -// use different segment parents based on namespace. Therefore we use constants instead of the string.
55 -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.
56 -const HTML_NAMESPACE = 1;
57 -const SVG_NAMESPACE = 2;
58 -const MATHML_NAMESPACE = 3;
59 -
60 -type NamespaceFlag = 0 | 1 | 2 | 3;
53 +// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
54 +// modes. We only include the variants as they matter for the sake of our purposes.
55 +// We don't actually provide the namespace therefore we use constants instead of the string.
56 +const HTML_MODE = 0;
57 +const SVG_MODE = 1;
58 +const MATHML_MODE = 2;
59 +const HTML_TABLE_MODE = 4;
60 +const HTML_TABLE_BODY_MODE = 5;
61 +const HTML_TABLE_ROW_MODE = 6;
62 +const HTML_COLGROUP_MODE = 7;
63 +// We have a greater than HTML_TABLE_MODE check elsewhere. If you add more cases here, make sure it
64 +// still makes sense
65 +
66 +type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
67
68 // Lets us keep track of contextual state and pick it back up after suspending.
69 export type FormatContext = {
64 - namespace: NamespaceFlag, // root/svg/html/mathml
70 + insertionMode: InsertionMode, // root/svg/html/mathml/table
71 selectedValue: null | string, // the selected value(s) inside a <select>, or null outside <select>
72 };
73
74 function createFormatContext(
69 - namespace: NamespaceFlag,
75 + insertionMode: InsertionMode,
76 selectedValue: null | string,
77 ): FormatContext {
78 return {
73 - namespace,
79 + insertionMode,
80 selectedValue,
81 };
82 }
83
78 -export function createRootFormatContext(): FormatContext {
79 - return createFormatContext(ROOT_NAMESPACE, null);
84 +export function createRootFormatContext(namespaceURI?: string): FormatContext {
85 + const insertionMode =
86 + namespaceURI === 'http://www.w3.org/2000/svg'
87 + ? SVG_MODE
88 + : namespaceURI === 'http://www.w3.org/1998/Math/MathML'
89 + ? MATHML_MODE
90 + : HTML_MODE;
91 + return createFormatContext(insertionMode, null);
92 }
93
94 export function getChildFormatContext(
@@ -87,15 +99,32 @@ export function getChildFormatContext(
99 switch (type) {
100 case 'select':
101 return createFormatContext(
90 - parentContext.namespace,
102 + HTML_MODE,
103 props.value != null ? props.value : props.defaultValue,
104 );
105 case 'svg':
94 - return createFormatContext(SVG_NAMESPACE, null);
106 + return createFormatContext(SVG_MODE, null);
107 case 'math':
96 - return createFormatContext(MATHML_NAMESPACE, null);
108 + return createFormatContext(MATHML_MODE, null);
109 case 'foreignObject':
98 - return createFormatContext(HTML_NAMESPACE, null);
110 + return createFormatContext(HTML_MODE, null);
111 + // Table parents are special in that their children can only be created at all if they're
112 + // wrapped in a table parent. So we need to encode that we're entering this mode.
113 + case 'table':
114 + return createFormatContext(HTML_TABLE_MODE, null);
115 + case 'thead':
116 + case 'tbody':
117 + case 'tfoot':
118 + return createFormatContext(HTML_TABLE_BODY_MODE, null);
119 + case 'colgroup':
120 + return createFormatContext(HTML_COLGROUP_MODE, null);
121 + case 'tr':
122 + return createFormatContext(HTML_TABLE_ROW_MODE, null);
123 + }
124 + if (parentContext.insertionMode >= HTML_TABLE_MODE) {
125 + // Whatever tag this was, it wasn't a table parent or other special parent, so we must have
126 + // entered plain HTML again.
127 + return createFormatContext(HTML_MODE, null);
128 }
129 return parentContext;
130 }
@@ -132,8 +161,8 @@ function assignAnID(
161 ));
162 }
163
135 -const dummyNode1 = stringToPrecomputedChunk('<span hidden id="');
136 -const dummyNode2 = stringToPrecomputedChunk('"></span>');
164 +const dummyNode1 = stringToPrecomputedChunk('<template id="');
165 +const dummyNode2 = stringToPrecomputedChunk('"></template>');
166
167 function pushDummyNodeWithID(
168 target: Array<Chunk | PrecomputedChunk>,
@@ -206,7 +235,20 @@ export function pushStartInstance(
235 startTag2,
236 );
237 } else {
209 - target.push(startTag1, stringToChunk(type), startTag2);
238 + target.push(startTag1, stringToChunk(type));
239 + if (props.className) {
240 + target.push(
241 + stringToChunk(
242 + ' class="' + encodeHTMLIDAttribute(props.className) + '"',
243 + ),
244 + );
245 + }
246 + if (props.id) {
247 + target.push(
248 + stringToChunk(' id="' + encodeHTMLIDAttribute(props.id) + '"'),
249 + );
250 + }
251 + target.push(startTag2);
252 }
253 }
254
@@ -225,16 +267,15 @@ export function pushEndInstance(
267 // Structural Nodes
268
269 // A placeholder is a node inside a hidden partial tree that can be filled in later, but before
228 -// display. It's never visible to users.
229 -const placeholder1 = stringToPrecomputedChunk('<span id="');
230 -const placeholder2 = stringToPrecomputedChunk('"></span>');
270 +// display. It's never visible to users. We use the template tag because it can be used in every
271 +// type of parent. <script> tags also work in every other tag except <colgroup>.
272 +const placeholder1 = stringToPrecomputedChunk('<template id="');
273 +const placeholder2 = stringToPrecomputedChunk('"></template>');
274 export function writePlaceholder(
275 destination: Destination,
276 responseState: ResponseState,
277 id: number,
278 ): boolean {
236 - // TODO: This needs to be contextually aware and switch tag since not all parents allow for spans like
237 - // <select> or <tbody>. E.g. suspending a component that renders a table row.
279 writeChunk(destination, placeholder1);
280 writeChunk(destination, responseState.placeholderPrefix);
281 const formattedID = stringToChunk(id.toString(16));
@@ -272,23 +313,130 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
313 return writeChunk(destination, endSuspenseBoundary);
314 }
315
275 -const startSegment = stringToPrecomputedChunk('<div hidden id="');
276 -const startSegment2 = stringToPrecomputedChunk('">');
277 -const endSegment = stringToPrecomputedChunk('</div>');
316 +const startSegmentHTML = stringToPrecomputedChunk('<div hidden id="');
317 +const startSegmentHTML2 = stringToPrecomputedChunk('">');
318 +const endSegmentHTML = stringToPrecomputedChunk('</div>');
319 +
320 +const startSegmentSVG = stringToPrecomputedChunk(
321 + '<svg aria-hidden="true" style="display:none" id="',
322 +);
323 +const startSegmentSVG2 = stringToPrecomputedChunk('">');
324 +const endSegmentSVG = stringToPrecomputedChunk('</svg>');
325 +
326 +const startSegmentMathML = stringToPrecomputedChunk(
327 + '<math aria-hidden="true" style="display:none" id="',
328 +);
329 +const startSegmentMathML2 = stringToPrecomputedChunk('">');
330 +const endSegmentMathML = stringToPrecomputedChunk('</math>');
331 +
332 +const startSegmentTable = stringToPrecomputedChunk('<table hidden id="');
333 +const startSegmentTable2 = stringToPrecomputedChunk('">');
334 +const endSegmentTable = stringToPrecomputedChunk('</table>');
335 +
336 +const startSegmentTableBody = stringToPrecomputedChunk(
337 + '<table hidden><tbody id="',
338 +);
339 +const startSegmentTableBody2 = stringToPrecomputedChunk('">');
340 +const endSegmentTableBody = stringToPrecomputedChunk('</tbody></table>');
341 +
342 +const startSegmentTableRow = stringToPrecomputedChunk('<table hidden><tr id="');
343 +const startSegmentTableRow2 = stringToPrecomputedChunk('">');
344 +const endSegmentTableRow = stringToPrecomputedChunk('</tr></table>');
345 +
346 +const startSegmentColGroup = stringToPrecomputedChunk(
347 + '<table hidden><colgroup id="',
348 +);
349 +const startSegmentColGroup2 = stringToPrecomputedChunk('">');
350 +const endSegmentColGroup = stringToPrecomputedChunk('</colgroup></table>');
351 +
352 export function writeStartSegment(
353 destination: Destination,
354 responseState: ResponseState,
355 + formatContext: FormatContext,
356 id: number,
357 ): boolean {
283 - // TODO: What happens with special children like <tr> if they're inserted in a div? Maybe needs contextually aware containers.
284 - writeChunk(destination, startSegment);
285 - writeChunk(destination, responseState.segmentPrefix);
286 - const formattedID = stringToChunk(id.toString(16));
287 - writeChunk(destination, formattedID);
288 - return writeChunk(destination, startSegment2);
358 + switch (formatContext.insertionMode) {
359 + case HTML_MODE: {
360 + writeChunk(destination, startSegmentHTML);
361 + writeChunk(destination, responseState.segmentPrefix);
362 + writeChunk(destination, stringToChunk(id.toString(16)));
363 + return writeChunk(destination, startSegmentHTML2);
364 + }
365 + case SVG_MODE: {
366 + writeChunk(destination, startSegmentSVG);
367 + writeChunk(destination, responseState.segmentPrefix);
368 + writeChunk(destination, stringToChunk(id.toString(16)));
369 + return writeChunk(destination, startSegmentSVG2);
370 + }
371 + case MATHML_MODE: {
372 + writeChunk(destination, startSegmentMathML);
373 + writeChunk(destination, responseState.segmentPrefix);
374 + writeChunk(destination, stringToChunk(id.toString(16)));
375 + return writeChunk(destination, startSegmentMathML2);
376 + }
377 + case HTML_TABLE_MODE: {
378 + writeChunk(destination, startSegmentTable);
379 + writeChunk(destination, responseState.segmentPrefix);
380 + writeChunk(destination, stringToChunk(id.toString(16)));
381 + return writeChunk(destination, startSegmentTable2);
382 + }
383 + // TODO: For the rest of these, there will be extra wrapper nodes that never
384 + // get deleted from the document. We need to delete the table too as part
385 + // of the injected scripts. They are invisible though so it's not too terrible
386 + // and it's kind of an edge case to suspend in a table. Totally supported though.
387 + case HTML_TABLE_BODY_MODE: {
388 + writeChunk(destination, startSegmentTableBody);
389 + writeChunk(destination, responseState.segmentPrefix);
390 + writeChunk(destination, stringToChunk(id.toString(16)));
391 + return writeChunk(destination, startSegmentTableBody2);
392 + }
393 + case HTML_TABLE_ROW_MODE: {
394 + writeChunk(destination, startSegmentTableRow);
395 + writeChunk(destination, responseState.segmentPrefix);
396 + writeChunk(destination, stringToChunk(id.toString(16)));
397 + return writeChunk(destination, startSegmentTableRow2);
398 + }
399 + case HTML_COLGROUP_MODE: {
400 + writeChunk(destination, startSegmentColGroup);
401 + writeChunk(destination, responseState.segmentPrefix);
402 + writeChunk(destination, stringToChunk(id.toString(16)));
403 + return writeChunk(destination, startSegmentColGroup2);
404 + }
405 + default: {
406 + invariant(false, 'Unknown insertion mode. This is a bug in React.');
407 + }
408 + }
409 }
290 -export function writeEndSegment(destination: Destination): boolean {
291 - return writeChunk(destination, endSegment);
410 +export function writeEndSegment(
411 + destination: Destination,
412 + formatContext: FormatContext,
413 +): boolean {
414 + switch (formatContext.insertionMode) {
415 + case HTML_MODE: {
416 + return writeChunk(destination, endSegmentHTML);
417 + }
418 + case SVG_MODE: {
419 + return writeChunk(destination, endSegmentSVG);
420 + }
421 + case MATHML_MODE: {
422 + return writeChunk(destination, endSegmentMathML);
423 + }
424 + case HTML_TABLE_MODE: {
425 + return writeChunk(destination, endSegmentTable);
426 + }
427 + case HTML_TABLE_BODY_MODE: {
428 + return writeChunk(destination, endSegmentTableBody);
429 + }
430 + case HTML_TABLE_ROW_MODE: {
431 + return writeChunk(destination, endSegmentTableRow);
432 + }
433 + case HTML_COLGROUP_MODE: {
434 + return writeChunk(destination, endSegmentColGroup);
435 + }
436 + default: {
437 + invariant(false, 'Unknown insertion mode. This is a bug in React.');
438 + }
439 + }
440 }
441
442 // Instruction Set
packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+5 -1
@@ -208,12 +208,16 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
208 export function writeStartSegment(
209 destination: Destination,
210 responseState: ResponseState,
211 + formatContext: FormatContext,
212 id: number,
213 ): boolean {
214 writeChunk(destination, SEGMENT);
215 return writeChunk(destination, formatID(id));
216 }
216 -export function writeEndSegment(destination: Destination): boolean {
217 +export function writeEndSegment(
218 + destination: Destination,
219 + formatContext: FormatContext,
220 +): boolean {
221 return writeChunk(destination, END);
222 }
223
packages/react-noop-renderer/src/ReactNoopServer.js
+2 -1
@@ -161,6 +161,7 @@ const ReactNoopServer = ReactFizzServer({
161 writeStartSegment(
162 destination: Destination,
163 responseState: ResponseState,
164 + formatContext: null,
165 id: number,
166 ): boolean {
167 const segment = {
@@ -172,7 +173,7 @@ const ReactNoopServer = ReactFizzServer({
173 }
174 destination.stack.push(segment);
175 },
175 - writeEndSegment(destination: Destination): boolean {
176 + writeEndSegment(destination: Destination, formatContext: null): boolean {
177 destination.stack.pop();
178 },
179
packages/react-server/src/ReactFizzServer.js
+31 -16
@@ -71,7 +71,6 @@ type Task = {
71 blockedBoundary: Root | SuspenseBoundary,
72 blockedSegment: Segment, // the segment we'll write to
73 abortSet: Set<Task>, // the abortable set that this task belongs to
74 - formatContext: FormatContext,
74 assignID: null | SuspenseBoundaryID, // id to assign to the content
75 };
76
@@ -90,6 +89,8 @@ type Segment = {
89 +index: number, // the index within the parent's chunks or 0 at the root
90 +chunks: Array<Chunk | PrecomputedChunk>,
91 +children: Array<Segment>,
92 + // The context that this segment was created in.
93 + formatContext: FormatContext,
94 // If this segment represents a fallback, this is the content that will replace that fallback.
95 +boundary: null | SuspenseBoundary,
96 };
@@ -172,7 +173,7 @@ export function createRequest(
173 onReadyToStream,
174 };
175 // This segment represents the root fallback.
175 - const rootSegment = createPendingSegment(request, 0, null);
176 + const rootSegment = createPendingSegment(request, 0, null, rootContext);
177 // There is no parent so conceptually, we're unblocked to flush this segment.
178 rootSegment.parentFlushed = true;
179 const rootTask = createTask(
@@ -181,7 +182,6 @@ export function createRequest(
182 null,
183 rootSegment,
184 abortSet,
184 - rootContext,
185 null,
186 );
187 pingedTasks.push(rootTask);
@@ -218,7 +218,6 @@ function createTask(
218 blockedBoundary: Root | SuspenseBoundary,
219 blockedSegment: Segment,
220 abortSet: Set<Task>,
221 - formatContext: FormatContext,
221 assignID: null | SuspenseBoundaryID,
222 ): Task {
223 request.allPendingTasks++;
@@ -233,7 +232,6 @@ function createTask(
232 blockedBoundary,
233 blockedSegment,
234 abortSet,
236 - formatContext,
235 assignID,
236 };
237 abortSet.add(task);
@@ -244,6 +242,7 @@ function createPendingSegment(
242 request: Request,
243 index: number,
244 boundary: null | SuspenseBoundary,
245 + formatContext: FormatContext,
246 ): Segment {
247 return {
248 status: PENDING,
@@ -252,6 +251,7 @@ function createPendingSegment(
251 parentFlushed: false,
252 chunks: [],
253 children: [],
254 + formatContext,
255 boundary,
256 };
257 }
@@ -317,7 +317,12 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
317 // Something suspended, we'll need to create a new segment and resolve it later.
318 const segment = task.blockedSegment;
319 const insertionIndex = segment.chunks.length;
320 - const newSegment = createPendingSegment(request, insertionIndex, null);
320 + const newSegment = createPendingSegment(
321 + request,
322 + insertionIndex,
323 + null,
324 + segment.formatContext,
325 + );
326 segment.children.push(newSegment);
327 const newTask = createTask(
328 request,
@@ -325,7 +330,6 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
330 task.blockedBoundary,
331 newSegment,
332 task.abortSet,
328 - task.formatContext,
333 task.assignID,
334 );
335 // We've delegated the assignment.
@@ -338,8 +342,9 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
342 }
343 }
344 } else if (typeof type === 'string') {
345 + const segment = task.blockedSegment;
346 pushStartInstance(
342 - task.blockedSegment.chunks,
347 + segment.chunks,
348 type,
349 props,
350 request.responseState,
@@ -347,13 +352,13 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
352 );
353 // We must have assigned it already above so we don't need this anymore.
354 task.assignID = null;
350 - const prevContext = task.formatContext;
351 - task.formatContext = getChildFormatContext(prevContext, type, props);
355 + const prevContext = segment.formatContext;
356 + segment.formatContext = getChildFormatContext(prevContext, type, props);
357 renderNode(request, task, props.children);
358 // We expect that errors will fatal the whole task and that we don't need
359 // the correct context. Therefore this is not in a finally.
355 - task.formatContext = prevContext;
356 - pushEndInstance(task.blockedSegment.chunks, type, props);
360 + segment.formatContext = prevContext;
361 + pushEndInstance(segment.chunks, type, props);
362 } else if (type === REACT_SUSPENSE_TYPE) {
363 const parentBoundary = task.blockedBoundary;
364 const parentSegment = task.blockedSegment;
@@ -376,11 +381,17 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
381 request,
382 insertionIndex,
383 newBoundary,
384 + parentSegment.formatContext,
385 );
386 parentSegment.children.push(boundarySegment);
387
388 // This segment is the actual child content. We can start rendering that immediately.
383 - const contentRootSegment = createPendingSegment(request, 0, null);
389 + const contentRootSegment = createPendingSegment(
390 + request,
391 + 0,
392 + null,
393 + parentSegment.formatContext,
394 + );
395 // We mark the root segment as having its parent flushed. It's not really flushed but there is
396 // no parent segment so there's nothing to wait on.
397 contentRootSegment.parentFlushed = true;
@@ -425,7 +436,6 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
436 parentBoundary,
437 boundarySegment,
438 fallbackAbortSet,
428 - task.formatContext,
439 newBoundary.id, // This is the ID we want to give this fallback so we can replace it later.
440 );
441 // TODO: This should be queued at a separate lower priority queue so that we only task
@@ -776,9 +786,14 @@ function flushSegmentContainer(
786 destination: Destination,
787 segment: Segment,
788 ): boolean {
779 - writeStartSegment(destination, request.responseState, segment.id);
789 + writeStartSegment(
790 + destination,
791 + request.responseState,
792 + segment.formatContext,
793 + segment.id,
794 + );
795 flushSegment(request, destination, segment);
781 - return writeEndSegment(destination);
796 + return writeEndSegment(destination, segment.formatContext);
797 }
798
799 function flushCompletedBoundary(
scripts/error-codes/codes.json
+2 -1
@@ -384,5 +384,6 @@
384 "393": "Cache cannot be refreshed during server rendering.",
385 "394": "startTransition cannot be called during server rendering.",
386 "395": "An ID must have been assigned before we can complete the boundary.",
387 - "396": "More boundaries or placeholders than we expected to ever emit."
387 + "396": "More boundaries or placeholders than we expected to ever emit.",
388 + "397": "Unknown insertion mode. This is a bug in React."
389 }