Skip to content

Commit

Permalink
[Fizz] Fragments and Iterable support (facebook#21228)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage authored and koto committed Jun 15, 2021
1 parent cd2b461 commit 0a060b6
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 11 deletions.
16 changes: 9 additions & 7 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,10 +351,12 @@ describe('ReactDOMFizzServer', () => {
await act(async () => {
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
<Suspense fallback={<Text text="Loading A..." />}>
<Text text="This will show A: " />
<div>
<AsyncText text="A" />
</div>
<>
<Text text="This will show A: " />
<div>
<AsyncText text="A" />
</div>
</>
</Suspense>,
writableA,
{
Expand Down Expand Up @@ -432,11 +434,11 @@ describe('ReactDOMFizzServer', () => {
}

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

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

function App() {
Expand Down Expand Up @@ -601,7 +603,7 @@ describe('ReactDOMFizzServer', () => {
// @gate experimental
it('can stream into an SVG container', async () => {
function AsyncPath({id}) {
return <path id={readText(id)}>{[]}</path>;
return <path id={readText(id)} />;
}

function App() {
Expand Down
97 changes: 93 additions & 4 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ import {
REACT_PORTAL_TYPE,
REACT_LAZY_TYPE,
REACT_SUSPENSE_TYPE,
REACT_LEGACY_HIDDEN_TYPE,
REACT_DEBUG_TRACING_MODE_TYPE,
REACT_STRICT_MODE_TYPE,
REACT_PROFILER_TYPE,
REACT_SUSPENSE_LIST_TYPE,
REACT_FRAGMENT_TYPE,
} from 'shared/ReactSymbols';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
Expand Down Expand Up @@ -521,6 +527,8 @@ const didWarnAboutContextTypeOnFunctionComponent = {};
const didWarnAboutGetDerivedStateOnFunctionComponent = {};
let didWarnAboutReassigningProps = false;
const didWarnAboutDefaultPropsOnFunctionComponent = {};
let didWarnAboutGenerators = false;
let didWarnAboutMaps = false;

// This would typically be a function component but we still support module pattern
// components for some reason.
Expand Down Expand Up @@ -701,10 +709,67 @@ function renderElement(
}
} else if (typeof type === 'string') {
renderHostElement(request, task, type, props);
} else if (type === REACT_SUSPENSE_TYPE) {
renderSuspenseBoundary(request, task, props);
} else {
throw new Error('Not yet implemented element type.');
switch (type) {
// TODO: LegacyHidden acts the same as a fragment. This only works
// because we currently assume that every instance of LegacyHidden is
// accompanied by a host component wrapper. In the hidden mode, the host
// component is given a `hidden` attribute, which ensures that the
// initial HTML is not visible. To support the use of LegacyHidden as a
// true fragment, without an extra DOM node, we would have to hide the
// initial HTML in some other way.
// TODO: Add REACT_OFFSCREEN_TYPE here too with the same capability.
case REACT_LEGACY_HIDDEN_TYPE:
case REACT_DEBUG_TRACING_MODE_TYPE:
case REACT_STRICT_MODE_TYPE:
case REACT_PROFILER_TYPE:
case REACT_SUSPENSE_LIST_TYPE: // TODO: SuspenseList should control the boundaries.
case REACT_FRAGMENT_TYPE: {
renderNodeDestructive(request, task, props.children);
break;
}
case REACT_SUSPENSE_TYPE: {
renderSuspenseBoundary(request, task, props);
break;
}
default: {
throw new Error('Not yet implemented element type.');
}
}
}
}

function validateIterable(iterable, iteratorFn: Function): void {
if (__DEV__) {
// We don't support rendering Generators because it's a mutation.
// See https://github.com/facebook/react/issues/12995
if (
typeof Symbol === 'function' &&
// $FlowFixMe Flow doesn't know about toStringTag
iterable[Symbol.toStringTag] === 'Generator'
) {
if (!didWarnAboutGenerators) {
console.error(
'Using Generators as children is unsupported and will likely yield ' +
'unexpected results because enumerating a generator mutates it. ' +
'You may convert it to an array with `Array.from()` or the ' +
'`[...spread]` operator before rendering. Keep in mind ' +
'you might need to polyfill these features for older browsers.',
);
}
didWarnAboutGenerators = true;
}

// Warn about using Maps as children
if ((iterable: any).entries === iteratorFn) {
if (!didWarnAboutMaps) {
console.error(
'Using Maps as children is not supported. ' +
'Use an array of keyed ReactElements instead.',
);
}
didWarnAboutMaps = true;
}
}
}

Expand Down Expand Up @@ -756,7 +821,30 @@ function renderNodeDestructive(

const iteratorFn = getIteratorFn(node);
if (iteratorFn) {
throw new Error('Not yet implemented node type.');
if (__DEV__) {
validateIterable(node, iteratorFn());
}
const iterator = iteratorFn.call(node);
if (iterator) {
let step = iterator.next();
// If there are not entries, we need to push an empty so we start by checking that.
if (!step.done) {
do {
// Recursively render the rest. We need to use the non-destructive form
// so that we can safely pop back up and render the sibling if something
// suspends.
renderNode(request, task, step.value);
step = iterator.next();
} while (!step.done);
return;
}
}
pushEmpty(
task.blockedSegment.chunks,
request.responseState,
task.assignID,
);
task.assignID = null;
}

const childString = Object.prototype.toString.call(node);
Expand Down Expand Up @@ -805,6 +893,7 @@ function renderNodeDestructive(