Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fizz] Fragments and Iterable support #21228

Merged
merged 4 commits into from
Apr 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(

// Any other type is assumed to be empty.
pushEmpty(task.blockedSegment.chunks, request.responseState, task.assignID);
task.assignID = null;
}

function spawnNewSuspendedTask(
Expand Down