Skip to content

Commit 32d6f39

Browse files
authoredMar 27, 2021
[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
1 parent a5aa9d5 commit 32d6f39

File tree

8 files changed

+475
-59
lines changed

8 files changed

+475
-59
lines changed
 

‎packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 247 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,22 @@ describe('ReactDOMFizzServer', () => {
9797
let node = element.firstChild;
9898
while (node) {
9999
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+
) {
101106
const props = {};
102107
const attributes = node.attributes;
103108
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+
}
104116
props[attributes[i].name] = attributes[i].value;
105117
}
106118
props.children = getVisibleChildren(node);
@@ -112,7 +124,7 @@ describe('ReactDOMFizzServer', () => {
112124
node = node.nextSibling;
113125
}
114126
return children.length === 0
115-
? null
127+
? undefined
116128
: children.length === 1
117129
? children[0]
118130
: children;
@@ -408,4 +420,237 @@ describe('ReactDOMFizzServer', () => {
408420
</div>,
409421
]);
410422
});
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+
});
411656
});

‎packages/react-dom/src/server/ReactDOMFizzServerBrowser.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323

2424
type Options = {
2525
identifierPrefix?: string,
26+
namespaceURI?: string,
2627
progressiveChunkSize?: number,
2728
signal?: AbortSignal,
2829
onReadyToStream?: () => void,
@@ -49,7 +50,7 @@ function renderToReadableStream(
4950
children,
5051
controller,
5152
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),
5354
options ? options.progressiveChunkSize : undefined,
5455
options ? options.onError : undefined,
5556
options ? options.onCompleteAll : undefined,

‎packages/react-dom/src/server/ReactDOMFizzServerNode.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ function createDrainHandler(destination, request) {
2828

2929
type Options = {
3030
identifierPrefix?: string,
31+
namespaceURI?: string,
3132
progressiveChunkSize?: number,
3233
onReadyToStream?: () => void,
3334
onCompleteAll?: () => void,
@@ -49,7 +50,7 @@ function pipeToNodeWritable(
4950
children,
5051
destination,
5152
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),
5354
options ? options.progressiveChunkSize : undefined,
5455
options ? options.onError : undefined,
5556
options ? options.onCompleteAll : undefined,

‎packages/react-dom/src/server/ReactDOMServerFormatConfig.js

Lines changed: 184 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -50,33 +50,45 @@ export function createResponseState(
5050
};
5151
}
5252

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;
6167

6268
// Lets us keep track of contextual state and pick it back up after suspending.
6369
export type FormatContext = {
64-
namespace: NamespaceFlag, // root/svg/html/mathml
70+
insertionMode: InsertionMode, // root/svg/html/mathml/table
6571
selectedValue: null | string, // the selected value(s) inside a <select>, or null outside <select>
6672
};
6773

6874
function createFormatContext(
69-
namespace: NamespaceFlag,
75+
insertionMode: InsertionMode,
7076
selectedValue: null | string,
7177
): FormatContext {
7278
return {
73-
namespace,
79+
insertionMode,
7480
selectedValue,
7581
};
7682
}
7783

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);
8092
}
8193

8294
export function getChildFormatContext(
@@ -87,15 +99,32 @@ export function getChildFormatContext(
8799
switch (type) {
88100
case 'select':
89101
return createFormatContext(
90-
parentContext.namespace,
102+
HTML_MODE,
91103
props.value != null ? props.value : props.defaultValue,
92104
);
93105
case 'svg':
94-
return createFormatContext(SVG_NAMESPACE, null);
106+
return createFormatContext(SVG_MODE, null);
95107
case 'math':
96-
return createFormatContext(MATHML_NAMESPACE, null);
108+
return createFormatContext(MATHML_MODE, null);
97109
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);
99128
}
100129
return parentContext;
101130
}
@@ -132,8 +161,8 @@ function assignAnID(
132161
));
133162
}
134163

135-
const dummyNode1 = stringToPrecomputedChunk('<span hidden id="');
136-
const dummyNode2 = stringToPrecomputedChunk('"></span>');
164+
const dummyNode1 = stringToPrecomputedChunk('<template id="');
165+
const dummyNode2 = stringToPrecomputedChunk('"></template>');
137166

138167
function pushDummyNodeWithID(
139168
target: Array<Chunk | PrecomputedChunk>,
@@ -206,7 +235,20 @@ export function pushStartInstance(
206235
startTag2,
207236
);
208237
} 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);
210252
}
211253
}
212254

@@ -225,16 +267,15 @@ export function pushEndInstance(
225267
// Structural Nodes
226268

227269
// 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>');
231274
export function writePlaceholder(
232275
destination: Destination,
233276
responseState: ResponseState,
234277
id: number,
235278
): 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.
238279
writeChunk(destination, placeholder1);
239280
writeChunk(destination, responseState.placeholderPrefix);
240281
const formattedID = stringToChunk(id.toString(16));
@@ -272,23 +313,130 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
272313
return writeChunk(destination, endSuspenseBoundary);
273314
}
274315

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+
278352
export function writeStartSegment(
279353
destination: Destination,
280354
responseState: ResponseState,
355+
formatContext: FormatContext,
281356
id: number,
282357
): 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+
}
289409
}
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+
}
292440
}
293441

294442
// Instruction Set

‎packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,16 @@ export function writeEndSuspenseBoundary(destination: Destination): boolean {
208208
export function writeStartSegment(
209209
destination: Destination,
210210
responseState: ResponseState,
211+
formatContext: FormatContext,
211212
id: number,
212213
): boolean {
213214
writeChunk(destination, SEGMENT);
214215
return writeChunk(destination, formatID(id));
215216
}
216-
export function writeEndSegment(destination: Destination): boolean {
217+
export function writeEndSegment(
218+
destination: Destination,
219+
formatContext: FormatContext,
220+
): boolean {
217221
return writeChunk(destination, END);
218222
}
219223

‎packages/react-noop-renderer/src/ReactNoopServer.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ const ReactNoopServer = ReactFizzServer({
161161
writeStartSegment(
162162
destination: Destination,
163163
responseState: ResponseState,
164+
formatContext: null,
164165
id: number,
165166
): boolean {
166167
const segment = {
@@ -172,7 +173,7 @@ const ReactNoopServer = ReactFizzServer({
172173
}
173174
destination.stack.push(segment);
174175
},
175-
writeEndSegment(destination: Destination): boolean {
176+
writeEndSegment(destination: Destination, formatContext: null): boolean {
176177
destination.stack.pop();
177178
},
178179

‎packages/react-server/src/ReactFizzServer.js

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ type Task = {
7171
blockedBoundary: Root | SuspenseBoundary,
7272
blockedSegment: Segment, // the segment we'll write to
7373
abortSet: Set<Task>, // the abortable set that this task belongs to
74-
formatContext: FormatContext,
7574
assignID: null | SuspenseBoundaryID, // id to assign to the content
7675
};
7776

@@ -90,6 +89,8 @@ type Segment = {
9089
+index: number, // the index within the parent's chunks or 0 at the root
9190
+chunks: Array<Chunk | PrecomputedChunk>,
9291
+children: Array<Segment>,
92+
// The context that this segment was created in.
93+
formatContext: FormatContext,
9394
// If this segment represents a fallback, this is the content that will replace that fallback.
9495
+boundary: null | SuspenseBoundary,
9596
};
@@ -172,7 +173,7 @@ export function createRequest(
172173
onReadyToStream,
173174
};
174175
// This segment represents the root fallback.
175-
const rootSegment = createPendingSegment(request, 0, null);
176+
const rootSegment = createPendingSegment(request, 0, null, rootContext);
176177
// There is no parent so conceptually, we're unblocked to flush this segment.
177178
rootSegment.parentFlushed = true;
178179
const rootTask = createTask(
@@ -181,7 +182,6 @@ export function createRequest(
181182
null,
182183
rootSegment,
183184
abortSet,
184-
rootContext,
185185
null,
186186
);
187187
pingedTasks.push(rootTask);
@@ -218,7 +218,6 @@ function createTask(
218218
blockedBoundary: Root | SuspenseBoundary,
219219
blockedSegment: Segment,
220220
abortSet: Set<Task>,
221-
formatContext: FormatContext,
222221
assignID: null | SuspenseBoundaryID,
223222
): Task {
224223
request.allPendingTasks++;
@@ -233,7 +232,6 @@ function createTask(
233232
blockedBoundary,
234233
blockedSegment,
235234
abortSet,
236-
formatContext,
237235
assignID,
238236
};
239237
abortSet.add(task);
@@ -244,6 +242,7 @@ function createPendingSegment(
244242
request: Request,
245243
index: number,
246244
boundary: null | SuspenseBoundary,
245+
formatContext: FormatContext,
247246
): Segment {
248247
return {
249248
status: PENDING,
@@ -252,6 +251,7 @@ function createPendingSegment(
252251
parentFlushed: false,
253252
chunks: [],
254253
children: [],
254+
formatContext,
255255
boundary,
256256
};
257257
}
@@ -317,15 +317,19 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
317317
// Something suspended, we'll need to create a new segment and resolve it later.
318318
const segment = task.blockedSegment;
319319
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+
);
321326
segment.children.push(newSegment);
322327
const newTask = createTask(
323328
request,
324329
node,
325330
task.blockedBoundary,
326331
newSegment,
327332
task.abortSet,
328-
task.formatContext,
329333
task.assignID,
330334
);
331335
// We've delegated the assignment.
@@ -338,22 +342,23 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
338342
}
339343
}
340344
} else if (typeof type === 'string') {
345+
const segment = task.blockedSegment;
341346
pushStartInstance(
342-
task.blockedSegment.chunks,
347+
segment.chunks,
343348
type,
344349
props,
345350
request.responseState,
346351
task.assignID,
347352
);
348353
// We must have assigned it already above so we don't need this anymore.
349354
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);
352357
renderNode(request, task, props.children);
353358
// We expect that errors will fatal the whole task and that we don't need
354359
// 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);
357362
} else if (type === REACT_SUSPENSE_TYPE) {
358363
const parentBoundary = task.blockedBoundary;
359364
const parentSegment = task.blockedSegment;
@@ -376,11 +381,17 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
376381
request,
377382
insertionIndex,
378383
newBoundary,
384+
parentSegment.formatContext,
379385
);
380386
parentSegment.children.push(boundarySegment);
381387

382388
// 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+
);
384395
// We mark the root segment as having its parent flushed. It's not really flushed but there is
385396
// no parent segment so there's nothing to wait on.
386397
contentRootSegment.parentFlushed = true;
@@ -425,7 +436,6 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
425436
parentBoundary,
426437
boundarySegment,
427438
fallbackAbortSet,
428-
task.formatContext,
429439
newBoundary.id, // This is the ID we want to give this fallback so we can replace it later.
430440
);
431441
// TODO: This should be queued at a separate lower priority queue so that we only task
@@ -776,9 +786,14 @@ function flushSegmentContainer(
776786
destination: Destination,
777787
segment: Segment,
778788
): boolean {
779-
writeStartSegment(destination, request.responseState, segment.id);
789+
writeStartSegment(
790+
destination,
791+
request.responseState,
792+
segment.formatContext,
793+
segment.id,
794+
);
780795
flushSegment(request, destination, segment);
781-
return writeEndSegment(destination);
796+
return writeEndSegment(destination, segment.formatContext);
782797
}
783798

784799
function flushCompletedBoundary(

‎scripts/error-codes/codes.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,5 +384,6 @@
384384
"393": "Cache cannot be refreshed during server rendering.",
385385
"394": "startTransition cannot be called during server rendering.",
386386
"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."
388389
}

0 commit comments

Comments
 (0)
Please sign in to comment.