Skip to content

Commit

Permalink
[Fizz] Support special HTML/SVG/MathML tags to suspend (facebook#21113)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sebmarkbage authored and acdlite committed Apr 11, 2021
1 parent 83d8538 commit 20c9f4f
Show file tree
Hide file tree
Showing 8 changed files with 475 additions and 59 deletions.
249 changes: 247 additions & 2 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,22 @@ 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') &&
!node.hasAttribute('aria-hidden')
) {
const props = {};
const attributes = node.attributes;
for (let i = 0; i < attributes.length; i++) {
if (
attributes[i].name === 'id' &&
attributes[i].value.includes(':')
) {
// We assume this is a React added ID that's a non-visual implementation detail.
continue;
}
props[attributes[i].name] = attributes[i].value;
}
props.children = getVisibleChildren(node);
Expand All @@ -112,7 +124,7 @@ describe('ReactDOMFizzServer', () => {
node = node.nextSibling;
}
return children.length === 0
? null
? undefined
: children.length === 1
? children[0]
: children;
Expand Down Expand Up @@ -408,4 +420,237 @@ describe('ReactDOMFizzServer', () => {
</div>,
]);
});

// @gate experimental
it('can resolve async content in esoteric parents', async () => {
function AsyncOption({text}) {
return <option>{readText(text)}</option>;
}

function AsyncCol({className}) {
return <col className={readText(className)}>{[]}</col>;
}

function AsyncPath({id}) {
return <path id={readText(id)}>{[]}</path>;
}

function AsyncMi({id}) {
return <mi id={readText(id)}>{[]}</mi>;
}

function App() {
return (
<div>
<select>
<Suspense fallback="Loading...">
<AsyncOption text="Hello" />
</Suspense>
</select>
<Suspense fallback="Loading...">
<table>
<colgroup>
<AsyncCol className="World" />
</colgroup>
</table>
<svg>
<g>
<AsyncPath id="my-path" />
</g>
</svg>
<math>
<AsyncMi id="my-mi" />
</math>
</Suspense>
</div>
);
}

await act(async () => {
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
<App />,
writable,
);
startWriting();
});

expect(getVisibleChildren(container)).toEqual(
<div>
<select>Loading...</select>Loading...
</div>,
);

await act(async () => {
resolveText('Hello');
});

await act(async () => {
resolveText('World');
});

await act(async () => {
resolveText('my-path');
resolveText('my-mi');
});

expect(getVisibleChildren(container)).toEqual(
<div>
<select>
<option>Hello</option>
</select>
<table>
<colgroup>
<col class="World" />
</colgroup>
</table>
<svg>
<g>
<path id="my-path" />
</g>
</svg>
<math>
<mi id="my-mi" />
</math>
</div>,
);

expect(container.querySelector('#my-path').namespaceURI).toBe(
'http://www.w3.org/2000/svg',
);
expect(container.querySelector('#my-mi').namespaceURI).toBe(
'http://www.w3.org/1998/Math/MathML',
);
});

// @gate experimental
it('can resolve async content in table parents', async () => {
function AsyncTableBody({className, children}) {
return <tbody className={readText(className)}>{children}</tbody>;
}

function AsyncTableRow({className, children}) {
return <tr className={readText(className)}>{children}</tr>;
}

function AsyncTableCell({text}) {
return <td>{readText(text)}</td>;
}

function App() {
return (
<table>
<Suspense
fallback={
<tbody>
<tr>
<td>Loading...</td>
</tr>
</tbody>
}>
<AsyncTableBody className="A">
<AsyncTableRow className="B">
<AsyncTableCell text="C" />
</AsyncTableRow>
</AsyncTableBody>
</Suspense>
</table>
);
}

await act(async () => {
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
<App />,
writable,
);
startWriting();
});

expect(getVisibleChildren(container)).toEqual(
<table>
<tbody>
<tr>
<td>Loading...</td>
</tr>
</tbody>
</table>,
);

await act(async () => {
resolveText('A');
});

await act(async () => {
resolveText('B');
});

await act(async () => {
resolveText('C');
});

expect(getVisibleChildren(container)).toEqual(
<table>
<tbody class="A">
<tr class="B">
<td>C</td>
</tr>
</tbody>
</table>,
);
});

// @gate experimental
it('can stream into an SVG container', async () => {
function AsyncPath({id}) {
return <path id={readText(id)}>{[]}</path>;
}

function App() {
return (
<g>
<Suspense fallback={<text>Loading...</text>}>
<AsyncPath id="my-path" />
</Suspense>
</g>
);
}

await act(async () => {
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
<App />,
writable,
{
namespaceURI: 'http://www.w3.org/2000/svg',
onReadyToStream() {
writable.write('<svg>');
startWriting();
writable.write('</svg>');
},
},
);
});

expect(getVisibleChildren(container)).toEqual(
<svg>
<g>
<text>Loading...</text>
</g>
</svg>,
);

await act(async () => {
resolveText('my-path');
});

expect(getVisibleChildren(container)).toEqual(
<svg>
<g>
<path id="my-path" />
</g>
</svg>,
);

expect(container.querySelector('#my-path').namespaceURI).toBe(
'http://www.w3.org/2000/svg',
);
});
});
3 changes: 2 additions & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {

type Options = {
identifierPrefix?: string,
namespaceURI?: string,
progressiveChunkSize?: number,
signal?: AbortSignal,
onReadyToStream?: () => void,
Expand All @@ -49,7 +50,7 @@ function renderToReadableStream(
children,
controller,
createResponseState(options ? options.identifierPrefix : undefined),
createRootFormatContext(), // We call this here in case we need options to initialize it.
createRootFormatContext(options ? options.namespaceURI : undefined),
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
options ? options.onCompleteAll : undefined,
Expand Down
3 changes: 2 additions & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function createDrainHandler(destination, request) {

type Options = {
identifierPrefix?: string,
namespaceURI?: string,
progressiveChunkSize?: number,
onReadyToStream?: () => void,
onCompleteAll?: () => void,
Expand All @@ -49,7 +50,7 @@ function pipeToNodeWritable(
children,
destination,
createResponseState(options ? options.identifierPrefix : undefined),
createRootFormatContext(), // We call this here in case we need options to initialize it.
createRootFormatContext(options ? options.namespaceURI : undefined),
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
options ? options.onCompleteAll : undefined,
Expand Down
Loading

0 comments on commit 20c9f4f

Please sign in to comment.