Skip to content

Commit 343710c

Browse files
authored
[Fizz] Fragments and Iterable support (#21228)
1 parent 933880b commit 343710c

File tree

2 files changed

+102
-11
lines changed

2 files changed

+102
-11
lines changed

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,12 @@ describe('ReactDOMFizzServer', () => {
351351
await act(async () => {
352352
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
353353
<Suspense fallback={<Text text="Loading A..." />}>
354-
<Text text="This will show A: " />
355-
<div>
356-
<AsyncText text="A" />
357-
</div>
354+
<>
355+
<Text text="This will show A: " />
356+
<div>
357+
<AsyncText text="A" />
358+
</div>
359+
</>
358360
</Suspense>,
359361
writableA,
360362
{
@@ -432,11 +434,11 @@ describe('ReactDOMFizzServer', () => {
432434
}
433435

434436
function AsyncPath({id}) {
435-
return <path id={readText(id)}>{[]}</path>;
437+
return <path id={readText(id)} />;
436438
}
437439

438440
function AsyncMi({id}) {
439-
return <mi id={readText(id)}>{[]}</mi>;
441+
return <mi id={readText(id)} />;
440442
}
441443

442444
function App() {
@@ -601,7 +603,7 @@ describe('ReactDOMFizzServer', () => {
601603
// @gate experimental
602604
it('can stream into an SVG container', async () => {
603605
function AsyncPath({id}) {
604-
return <path id={readText(id)}>{[]}</path>;
606+
return <path id={readText(id)} />;
605607
}
606608

607609
function App() {

packages/react-server/src/ReactFizzServer.js

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ import {
6262
REACT_PORTAL_TYPE,
6363
REACT_LAZY_TYPE,
6464
REACT_SUSPENSE_TYPE,
65+
REACT_LEGACY_HIDDEN_TYPE,
66+
REACT_DEBUG_TRACING_MODE_TYPE,
67+
REACT_STRICT_MODE_TYPE,
68+
REACT_PROFILER_TYPE,
69+
REACT_SUSPENSE_LIST_TYPE,
70+
REACT_FRAGMENT_TYPE,
6571
} from 'shared/ReactSymbols';
6672
import ReactSharedInternals from 'shared/ReactSharedInternals';
6773
import {
@@ -521,6 +527,8 @@ const didWarnAboutContextTypeOnFunctionComponent = {};
521527
const didWarnAboutGetDerivedStateOnFunctionComponent = {};
522528
let didWarnAboutReassigningProps = false;
523529
const didWarnAboutDefaultPropsOnFunctionComponent = {};
530+
let didWarnAboutGenerators = false;
531+
let didWarnAboutMaps = false;
524532

525533
// This would typically be a function component but we still support module pattern
526534
// components for some reason.
@@ -701,10 +709,67 @@ function renderElement(
701709
}
702710
} else if (typeof type === 'string') {
703711
renderHostElement(request, task, type, props);
704-
} else if (type === REACT_SUSPENSE_TYPE) {
705-
renderSuspenseBoundary(request, task, props);
706712
} else {
707-
throw new Error('Not yet implemented element type.');
713+
switch (type) {
714+
// TODO: LegacyHidden acts the same as a fragment. This only works
715+
// because we currently assume that every instance of LegacyHidden is
716+
// accompanied by a host component wrapper. In the hidden mode, the host
717+
// component is given a `hidden` attribute, which ensures that the
718+
// initial HTML is not visible. To support the use of LegacyHidden as a
719+
// true fragment, without an extra DOM node, we would have to hide the
720+
// initial HTML in some other way.
721+
// TODO: Add REACT_OFFSCREEN_TYPE here too with the same capability.
722+
case REACT_LEGACY_HIDDEN_TYPE:
723+
case REACT_DEBUG_TRACING_MODE_TYPE:
724+
case REACT_STRICT_MODE_TYPE:
725+
case REACT_PROFILER_TYPE:
726+
case REACT_SUSPENSE_LIST_TYPE: // TODO: SuspenseList should control the boundaries.
727+
case REACT_FRAGMENT_TYPE: {
728+
renderNodeDestructive(request, task, props.children);
729+
break;
730+
}
731+
case REACT_SUSPENSE_TYPE: {
732+
renderSuspenseBoundary(request, task, props);
733+
break;
734+
}
735+
default: {
736+
throw new Error('Not yet implemented element type.');
737+
}
738+
}
739+
}
740+
}
741+
742+
function validateIterable(iterable, iteratorFn: Function): void {
743+
if (__DEV__) {
744+
// We don't support rendering Generators because it's a mutation.
745+
// See https://github.com/facebook/react/issues/12995
746+
if (
747+
typeof Symbol === 'function' &&
748+
// $FlowFixMe Flow doesn't know about toStringTag
749+
iterable[Symbol.toStringTag] === 'Generator'
750+
) {
751+
if (!didWarnAboutGenerators) {
752+
console.error(
753+
'Using Generators as children is unsupported and will likely yield ' +
754+
'unexpected results because enumerating a generator mutates it. ' +
755+
'You may convert it to an array with `Array.from()` or the ' +
756+
'`[...spread]` operator before rendering. Keep in mind ' +
757+
'you might need to polyfill these features for older browsers.',
758+
);
759+
}
760+
didWarnAboutGenerators = true;
761+
}
762+
763+
// Warn about using Maps as children
764+
if ((iterable: any).entries === iteratorFn) {
765+
if (!didWarnAboutMaps) {
766+
console.error(
767+
'Using Maps as children is not supported. ' +
768+
'Use an array of keyed ReactElements instead.',
769+
);
770+
}
771+
didWarnAboutMaps = true;
772+
}
708773
}
709774
}
710775

@@ -756,7 +821,30 @@ function renderNodeDestructive(
756821

757822
const iteratorFn = getIteratorFn(node);
758823
if (iteratorFn) {
759-
throw new Error('Not yet implemented node type.');
824+
if (__DEV__) {
825+
validateIterable(node, iteratorFn());
826+
}
827+
const iterator = iteratorFn.call(node);
828+
if (iterator) {
829+
let step = iterator.next();
830+
// If there are not entries, we need to push an empty so we start by checking that.
831+
if (!step.done) {
832+
do {
833+
// Recursively render the rest. We need to use the non-destructive form
834+
// so that we can safely pop back up and render the sibling if something
835+
// suspends.
836+
renderNode(request, task, step.value);
837+
step = iterator.next();
838+
} while (!step.done);
839+
return;
840+
}
841+
}
842+
pushEmpty(
843+
task.blockedSegment.chunks,
844+
request.responseState,
845+
task.assignID,
846+
);
847+
task.assignID = null;
760848
}
761849

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

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

810899
function spawnNewSuspendedTask(

0 commit comments

Comments
 (0)