From 0a060b649b375d90b06e4b26074d9bea95e686d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Sat, 10 Apr 2021 15:50:42 -0400 Subject: [PATCH] [Fizz] Fragments and Iterable support (#21228) --- .../src/__tests__/ReactDOMFizzServer-test.js | 16 +-- packages/react-server/src/ReactFizzServer.js | 97 ++++++++++++++++++- 2 files changed, 102 insertions(+), 11 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 0b69bf72231a3..862c540c3b9f5 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -351,10 +351,12 @@ describe('ReactDOMFizzServer', () => { await act(async () => { const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable( }> - -
- -
+ <> + +
+ +
+
, writableA, { @@ -432,11 +434,11 @@ describe('ReactDOMFizzServer', () => { } function AsyncPath({id}) { - return {[]}; + return ; } function AsyncMi({id}) { - return {[]}; + return ; } function App() { @@ -601,7 +603,7 @@ describe('ReactDOMFizzServer', () => { // @gate experimental it('can stream into an SVG container', async () => { function AsyncPath({id}) { - return {[]}; + return ; } function App() { diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 90053ceb02512..3664e7ba48830 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -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 { @@ -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. @@ -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; + } } } @@ -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); @@ -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(