[Fizz] Fragments and Iterable support (#21228)
Sebastian Markbåge committed
Apr 10, 2021 at 15:50 UTC
343710c9230d643ec442acef4634e380c1f0bce9
2 files changed
+102
-11
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+9
-7
@@ -351,10 +351,12 @@ describe('ReactDOMFizzServer', () => {
351
await act(async () => {
352
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
353
<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
+ </>
360
</Suspense>,
361
writableA,
362
{
@@ -432,11 +434,11 @@ describe('ReactDOMFizzServer', () => {
434
}
435
436
function AsyncPath({id}) {
435
- return <path id={readText(id)}>{[]}</path>;
437
+ return <path id={readText(id)} />;
438
}
439
440
function AsyncMi({id}) {
439
- return <mi id={readText(id)}>{[]}</mi>;
441
+ return <mi id={readText(id)} />;
442
}
443
444
function App() {
@@ -601,7 +603,7 @@ describe('ReactDOMFizzServer', () => {
603
// @gate experimental
604
it('can stream into an SVG container', async () => {
605
function AsyncPath({id}) {
604
- return <path id={readText(id)}>{[]}</path>;
606
+ return <path id={readText(id)} />;
607
}
608
609
function App() {
packages/react-server/src/ReactFizzServer.js
+93
-4
@@ -62,6 +62,12 @@ import {
62
REACT_PORTAL_TYPE,
63
REACT_LAZY_TYPE,
64
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,
71
} from 'shared/ReactSymbols';
72
import ReactSharedInternals from 'shared/ReactSharedInternals';
73
import {
@@ -521,6 +527,8 @@ const didWarnAboutContextTypeOnFunctionComponent = {};
527
const didWarnAboutGetDerivedStateOnFunctionComponent = {};
528
let didWarnAboutReassigningProps = false;
529
const didWarnAboutDefaultPropsOnFunctionComponent = {};
530
+let didWarnAboutGenerators = false;
531
+let didWarnAboutMaps = false;
532
533
// This would typically be a function component but we still support module pattern
534
// components for some reason.
@@ -701,10 +709,67 @@ function renderElement(
709
}
710
} else if (typeof type === 'string') {
711
renderHostElement(request, task, type, props);
704
- } else if (type === REACT_SUSPENSE_TYPE) {
705
- renderSuspenseBoundary(request, task, props);
712
} 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
+ }
773
}
774
}
775
@@ -756,7 +821,30 @@ function renderNodeDestructive(
821
822
const iteratorFn = getIteratorFn(node);
823
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;
848
}
849
850
const childString = Object.prototype.toString.call(node);
@@ -805,6 +893,7 @@ function renderNodeDestructive(
893
894
// Any other type is assumed to be empty.
895
pushEmpty(task.blockedSegment.chunks, request.responseState, task.assignID);
896
+ task.assignID = null;
897
}
898
899
function spawnNewSuspendedTask(