diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js
index 6af8c1356f9ca..fcb2406552899 100644
--- a/packages/react-reconciler/src/ReactChildFiber.js
+++ b/packages/react-reconciler/src/ReactChildFiber.js
@@ -13,6 +13,7 @@ import type {
Thenable,
ReactContext,
ReactDebugInfo,
+ SuspenseListRevealOrder,
} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane';
@@ -2057,3 +2058,103 @@ export function resetChildFibers(workInProgress: Fiber, lanes: Lanes): void {
child = child.sibling;
}
}
+
+function validateSuspenseListNestedChild(childSlot: mixed, index: number) {
+ if (__DEV__) {
+ const isAnArray = isArray(childSlot);
+ const isIterable =
+ !isAnArray && typeof getIteratorFn(childSlot) === 'function';
+ const isAsyncIterable =
+ enableAsyncIterableChildren &&
+ typeof childSlot === 'object' &&
+ childSlot !== null &&
+ typeof (childSlot: any)[ASYNC_ITERATOR] === 'function';
+ if (isAnArray || isIterable || isAsyncIterable) {
+ const type = isAnArray
+ ? 'array'
+ : isAsyncIterable
+ ? 'async iterable'
+ : 'iterable';
+ console.error(
+ 'A nested %s was passed to row #%s in . Wrap it in ' +
+ 'an additional SuspenseList to configure its revealOrder: ' +
+ ' ... ' +
+ '{%s} ... ' +
+ '',
+ type,
+ index,
+ type,
+ );
+ return false;
+ }
+ }
+ return true;
+}
+
+export function validateSuspenseListChildren(
+ children: mixed,
+ revealOrder: SuspenseListRevealOrder,
+) {
+ if (__DEV__) {
+ if (
+ (revealOrder === 'forwards' || revealOrder === 'backwards') &&
+ children !== undefined &&
+ children !== null &&
+ children !== false
+ ) {
+ if (isArray(children)) {
+ for (let i = 0; i < children.length; i++) {
+ if (!validateSuspenseListNestedChild(children[i], i)) {
+ return;
+ }
+ }
+ } else {
+ const iteratorFn = getIteratorFn(children);
+ if (typeof iteratorFn === 'function') {
+ const childrenIterator = iteratorFn.call(children);
+ if (childrenIterator) {
+ let step = childrenIterator.next();
+ let i = 0;
+ for (; !step.done; step = childrenIterator.next()) {
+ if (!validateSuspenseListNestedChild(step.value, i)) {
+ return;
+ }
+ i++;
+ }
+ }
+ } else if (
+ enableAsyncIterableChildren &&
+ typeof (children: any)[ASYNC_ITERATOR] === 'function'
+ ) {
+ // TODO: Technically we should warn for nested arrays inside the
+ // async iterable but it would require unwrapping the array.
+ // However, this mistake is not as easy to make so it's ok not to warn.
+ } else if (
+ enableAsyncIterableChildren &&
+ children.$$typeof === REACT_ELEMENT_TYPE &&
+ typeof children.type === 'function' &&
+ // $FlowFixMe
+ (Object.prototype.toString.call(children.type) ===
+ '[object GeneratorFunction]' ||
+ // $FlowFixMe
+ Object.prototype.toString.call(children.type) ===
+ '[object AsyncGeneratorFunction]')
+ ) {
+ console.error(
+ 'A generator Component was passed to a . ' +
+ 'This is not supported as a way to generate lists. Instead, pass an ' +
+ 'iterable as the children.',
+ revealOrder,
+ );
+ } else {
+ console.error(
+ 'A single row was passed to a . ' +
+ 'This is not useful since it needs multiple rows. ' +
+ 'Did you mean to pass multiple children or an array?',
+ revealOrder,
+ );
+ }
+ }
+ }
+ }
+}
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index 69bc84038dac9..7b86962f778fe 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -123,7 +123,6 @@ import {
enableViewTransition,
enableFragmentRefs,
} from 'shared/ReactFeatureFlags';
-import isArray from 'shared/isArray';
import shallowEqual from 'shared/shallowEqual';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
import getComponentNameFromType from 'shared/getComponentNameFromType';
@@ -132,7 +131,6 @@ import {
REACT_LAZY_TYPE,
REACT_FORWARD_REF_TYPE,
REACT_MEMO_TYPE,
- getIteratorFn,
} from 'shared/ReactSymbols';
import {setCurrentFiber} from './ReactCurrentFiber';
import {
@@ -145,6 +143,7 @@ import {
mountChildFibers,
reconcileChildFibers,
cloneChildFibers,
+ validateSuspenseListChildren,
} from './ReactChildFiber';
import {
processUpdateQueue,
@@ -3302,73 +3301,6 @@ function validateTailOptions(
}
}
-function validateSuspenseListNestedChild(childSlot: mixed, index: number) {
- if (__DEV__) {
- const isAnArray = isArray(childSlot);
- const isIterable =
- !isAnArray && typeof getIteratorFn(childSlot) === 'function';
- if (isAnArray || isIterable) {
- const type = isAnArray ? 'array' : 'iterable';
- console.error(
- 'A nested %s was passed to row #%s in . Wrap it in ' +
- 'an additional SuspenseList to configure its revealOrder: ' +
- ' ... ' +
- '{%s} ... ' +
- '',
- type,
- index,
- type,
- );
- return false;
- }
- }
- return true;
-}
-
-function validateSuspenseListChildren(
- children: mixed,
- revealOrder: SuspenseListRevealOrder,
-) {
- if (__DEV__) {
- if (
- (revealOrder === 'forwards' || revealOrder === 'backwards') &&
- children !== undefined &&
- children !== null &&
- children !== false
- ) {
- if (isArray(children)) {
- for (let i = 0; i < children.length; i++) {
- if (!validateSuspenseListNestedChild(children[i], i)) {
- return;
- }
- }
- } else {
- const iteratorFn = getIteratorFn(children);
- if (typeof iteratorFn === 'function') {
- const childrenIterator = iteratorFn.call(children);
- if (childrenIterator) {
- let step = childrenIterator.next();
- let i = 0;
- for (; !step.done; step = childrenIterator.next()) {
- if (!validateSuspenseListNestedChild(step.value, i)) {
- return;
- }
- i++;
- }
- }
- } else {
- console.error(
- 'A single row was passed to a . ' +
- 'This is not useful since it needs multiple rows. ' +
- 'Did you mean to pass multiple children or an array?',
- revealOrder,
- );
- }
- }
- }
- }
-}
-
function initSuspenseListRenderState(
workInProgress: Fiber,
isBackwards: boolean,
@@ -3415,12 +3347,6 @@ function updateSuspenseListComponent(
const tailMode: SuspenseListTailMode = nextProps.tail;
const newChildren = nextProps.children;
- validateRevealOrder(revealOrder);
- validateTailOptions(tailMode, revealOrder);
- validateSuspenseListChildren(newChildren, revealOrder);
-
- reconcileChildren(current, workInProgress, newChildren, renderLanes);
-
let suspenseContext: SuspenseContext = suspenseStackCursor.current;
const shouldForceFallback = hasSuspenseListContext(
@@ -3434,6 +3360,17 @@ function updateSuspenseListComponent(
);
workInProgress.flags |= DidCapture;
} else {
+ suspenseContext = setDefaultShallowSuspenseListContext(suspenseContext);
+ }
+ pushSuspenseListContext(workInProgress, suspenseContext);
+
+ validateRevealOrder(revealOrder);
+ validateTailOptions(tailMode, revealOrder);
+ validateSuspenseListChildren(newChildren, revealOrder);
+
+ reconcileChildren(current, workInProgress, newChildren, renderLanes);
+
+ if (!shouldForceFallback) {
const didSuspendBefore =
current !== null && (current.flags & DidCapture) !== NoFlags;
if (didSuspendBefore) {
@@ -3446,9 +3383,7 @@ function updateSuspenseListComponent(
renderLanes,
);
}
- suspenseContext = setDefaultShallowSuspenseListContext(suspenseContext);
}
- pushSuspenseListContext(workInProgress, suspenseContext);
if (!disableLegacyMode && (workInProgress.mode & ConcurrentMode) === NoMode) {
// In legacy mode, SuspenseList doesn't work so we just
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
index 6faeae3acba0d..f9efb330cf891 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
@@ -3119,4 +3119,197 @@ describe('ReactSuspenseList', () => {
);
},
);
+
+ // @gate enableSuspenseList && enableAsyncIterableChildren
+ it('warns for async generator components in "forwards" order', async () => {
+ async function* Generator() {
+ yield 'A';
+ yield 'B';
+ }
+ function Foo() {
+ return (
+
+
+
+ );
+ }
+
+ await act(() => {
+ React.startTransition(() => {
+ ReactNoop.render();
+ });
+ });
+ assertConsoleErrorDev([
+ 'A generator Component was passed to a . ' +
+ 'This is not supported as a way to generate lists. Instead, pass an ' +
+ 'iterable as the children.' +
+ '\n in SuspenseList (at **)' +
+ '\n in Foo (at **)',
+ ' is an async Client Component. ' +
+ 'Only Server Components can be async at the moment. ' +
+ "This error is often caused by accidentally adding `'use client'` " +
+ 'to a module that was originally written for the server.\n' +
+ ' in Foo (at **)',
+ // We get this warning because the generator's promise themselves are not cached.
+ 'A component was suspended by an uncached promise. ' +
+ 'Creating promises inside a Client Component or hook is not yet supported, ' +
+ 'except via a Suspense-compatible library or framework.\n' +
+ ' in Foo (at **)',
+ ]);
+ });
+
+ // @gate enableSuspenseList && enableAsyncIterableChildren
+ it('can display async iterable in "forwards" order', async () => {
+ const A = createAsyncText('A');
+ const B = createAsyncText('B');
+
+ // We use Cached elements to avoid rerender.
+ const ASlot = (
+ }>
+
+
+ );
+
+ const BSlot = (
+ }>
+
+
+ );
+
+ const iterable = {
+ async *[Symbol.asyncIterator]() {
+ yield ASlot;
+ yield BSlot;
+ },
+ };
+
+ function Foo() {
+ return {iterable};
+ }
+
+ await act(() => {
+ React.startTransition(() => {
+ ReactNoop.render();
+ });
+ });
+
+ assertLog([
+ 'Suspend! [A]',
+ 'Loading A',
+ 'Loading B',
+ // pre-warming
+ 'Suspend! [A]',
+ ]);
+
+ assertConsoleErrorDev([
+ // We get this warning because the generator's promise themselves are not cached.
+ 'A component was suspended by an uncached promise. ' +
+ 'Creating promises inside a Client Component or hook is not yet supported, ' +
+ 'except via a Suspense-compatible library or framework.\n' +
+ ' in SuspenseList (at **)\n' +
+ ' in Foo (at **)',
+ 'A component was suspended by an uncached promise. ' +
+ 'Creating promises inside a Client Component or hook is not yet supported, ' +
+ 'except via a Suspense-compatible library or framework.\n' +
+ ' in SuspenseList (at **)\n' +
+ ' in Foo (at **)',
+ ]);
+
+ expect(ReactNoop).toMatchRenderedOutput(
+ <>
+ Loading A
+ Loading B
+ >,
+ );
+
+ await act(() => A.resolve());
+ assertLog(['A', 'Suspend! [B]', 'Suspend! [B]']);
+
+ assertConsoleErrorDev([
+ // We get this warning because the generator's promise themselves are not cached.
+ 'A component was suspended by an uncached promise. ' +
+ 'Creating promises inside a Client Component or hook is not yet supported, ' +
+ 'except via a Suspense-compatible library or framework.\n' +
+ ' in SuspenseList (at **)\n' +
+ ' in Foo (at **)',
+ 'A component was suspended by an uncached promise. ' +
+ 'Creating promises inside a Client Component or hook is not yet supported, ' +
+ 'except via a Suspense-compatible library or framework.\n' +
+ ' in SuspenseList (at **)\n' +
+ ' in Foo (at **)',
+ ]);
+
+ expect(ReactNoop).toMatchRenderedOutput(
+ <>
+ A
+ Loading B
+ >,
+ );
+
+ await act(() => B.resolve());
+ assertLog(['B']);
+
+ assertConsoleErrorDev([
+ // We get this warning because the generator's promise themselves are not cached.
+ 'A component was suspended by an uncached promise. ' +
+ 'Creating promises inside a Client Component or hook is not yet supported, ' +
+ 'except via a Suspense-compatible library or framework.\n' +
+ ' in SuspenseList (at **)\n' +
+ ' in Foo (at **)',
+ ]);
+
+ expect(ReactNoop).toMatchRenderedOutput(
+ <>
+ A
+ B
+ >,
+ );
+ });
+
+ // @gate enableSuspenseList && enableAsyncIterableChildren
+ it('warns if a nested async iterable is passed to a "forwards" list', async () => {
+ function Foo({items}) {
+ return (
+
+ {items}
+ Tail
+
+ );
+ }
+
+ const iterable = {
+ async *[Symbol.asyncIterator]() {
+ yield (
+
+ A
+
+ );
+ yield (
+
+ B
+
+ );
+ },
+ };
+
+ await act(() => {
+ React.startTransition(() => {
+ ReactNoop.render();
+ });
+ });
+ assertConsoleErrorDev([
+ 'A nested async iterable was passed to row #0 in . ' +
+ 'Wrap it in an additional SuspenseList to configure its revealOrder: ' +
+ ' ... ' +
+ '{async iterable} ... ' +
+ '' +
+ '\n in SuspenseList (at **)' +
+ '\n in Foo (at **)',
+ // We get this warning because the generator's promise themselves are not cached.
+ 'A component was suspended by an uncached promise. ' +
+ 'Creating promises inside a Client Component or hook is not yet supported, ' +
+ 'except via a Suspense-compatible library or framework.\n' +
+ ' in Foo (at **)',
+ ]);
+ });
});