Skip to content

Commit 32d6f39

Browse files
authored
[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,

0 commit comments

Comments
 (0)