Skip to content

Commit 4c6967b

Browse files
authored
[Fiber] Support AsyncIterable children in SuspenseList (#33299)
We support AsyncIterable (more so when it's a cached form like in coming from Flight) as children. This fixes some warnings and bugs when passed to SuspenseList. Ideally SuspenseList with `tail="hidden"` should support unblocking before the full result has resolved but that's an optimization on top. We also might want to change semantics for this for `revealOrder="backwards"` so it becomes possible to stream items in reverse order.
1 parent c6c2a52 commit 4c6967b

File tree

3 files changed

+306
-77
lines changed

3 files changed

+306
-77
lines changed

packages/react-reconciler/src/ReactChildFiber.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
Thenable,
1414
ReactContext,
1515
ReactDebugInfo,
16+
SuspenseListRevealOrder,
1617
} from 'shared/ReactTypes';
1718
import type {Fiber} from './ReactInternalTypes';
1819
import type {Lanes} from './ReactFiberLane';
@@ -2057,3 +2058,103 @@ export function resetChildFibers(workInProgress: Fiber, lanes: Lanes): void {
20572058
child = child.sibling;
20582059
}
20592060
}
2061+
2062+
function validateSuspenseListNestedChild(childSlot: mixed, index: number) {
2063+
if (__DEV__) {
2064+
const isAnArray = isArray(childSlot);
2065+
const isIterable =
2066+
!isAnArray && typeof getIteratorFn(childSlot) === 'function';
2067+
const isAsyncIterable =
2068+
enableAsyncIterableChildren &&
2069+
typeof childSlot === 'object' &&
2070+
childSlot !== null &&
2071+
typeof (childSlot: any)[ASYNC_ITERATOR] === 'function';
2072+
if (isAnArray || isIterable || isAsyncIterable) {
2073+
const type = isAnArray
2074+
? 'array'
2075+
: isAsyncIterable
2076+
? 'async iterable'
2077+
: 'iterable';
2078+
console.error(
2079+
'A nested %s was passed to row #%s in <SuspenseList />. Wrap it in ' +
2080+
'an additional SuspenseList to configure its revealOrder: ' +
2081+
'<SuspenseList revealOrder=...> ... ' +
2082+
'<SuspenseList revealOrder=...>{%s}</SuspenseList> ... ' +
2083+
'</SuspenseList>',
2084+
type,
2085+
index,
2086+
type,
2087+
);
2088+
return false;
2089+
}
2090+
}
2091+
return true;
2092+
}
2093+
2094+
export function validateSuspenseListChildren(
2095+
children: mixed,
2096+
revealOrder: SuspenseListRevealOrder,
2097+
) {
2098+
if (__DEV__) {
2099+
if (
2100+
(revealOrder === 'forwards' || revealOrder === 'backwards') &&
2101+
children !== undefined &&
2102+
children !== null &&
2103+
children !== false
2104+
) {
2105+
if (isArray(children)) {
2106+
for (let i = 0; i < children.length; i++) {
2107+
if (!validateSuspenseListNestedChild(children[i], i)) {
2108+
return;
2109+
}
2110+
}
2111+
} else {
2112+
const iteratorFn = getIteratorFn(children);
2113+
if (typeof iteratorFn === 'function') {
2114+
const childrenIterator = iteratorFn.call(children);
2115+
if (childrenIterator) {
2116+
let step = childrenIterator.next();
2117+
let i = 0;
2118+
for (; !step.done; step = childrenIterator.next()) {
2119+
if (!validateSuspenseListNestedChild(step.value, i)) {
2120+
return;
2121+
}
2122+
i++;
2123+
}
2124+
}
2125+
} else if (
2126+
enableAsyncIterableChildren &&
2127+
typeof (children: any)[ASYNC_ITERATOR] === 'function'
2128+
) {
2129+
// TODO: Technically we should warn for nested arrays inside the
2130+
// async iterable but it would require unwrapping the array.
2131+
// However, this mistake is not as easy to make so it's ok not to warn.
2132+
} else if (
2133+
enableAsyncIterableChildren &&
2134+
children.$$typeof === REACT_ELEMENT_TYPE &&
2135+
typeof children.type === 'function' &&
2136+
// $FlowFixMe
2137+
(Object.prototype.toString.call(children.type) ===
2138+
'[object GeneratorFunction]' ||
2139+
// $FlowFixMe
2140+
Object.prototype.toString.call(children.type) ===
2141+
'[object AsyncGeneratorFunction]')
2142+
) {
2143+
console.error(
2144+
'A generator Component was passed to a <SuspenseList revealOrder="%s" />. ' +
2145+
'This is not supported as a way to generate lists. Instead, pass an ' +
2146+
'iterable as the children.',
2147+
revealOrder,
2148+
);
2149+
} else {
2150+
console.error(
2151+
'A single row was passed to a <SuspenseList revealOrder="%s" />. ' +
2152+
'This is not useful since it needs multiple rows. ' +
2153+
'Did you mean to pass multiple children or an array?',
2154+
revealOrder,
2155+
);
2156+
}
2157+
}
2158+
}
2159+
}
2160+
}

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 12 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ import {
123123
enableViewTransition,
124124
enableFragmentRefs,
125125
} from 'shared/ReactFeatureFlags';
126-
import isArray from 'shared/isArray';
127126
import shallowEqual from 'shared/shallowEqual';
128127
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
129128
import getComponentNameFromType from 'shared/getComponentNameFromType';
@@ -132,7 +131,6 @@ import {
132131
REACT_LAZY_TYPE,
133132
REACT_FORWARD_REF_TYPE,
134133
REACT_MEMO_TYPE,
135-
getIteratorFn,
136134
} from 'shared/ReactSymbols';
137135
import {setCurrentFiber} from './ReactCurrentFiber';
138136
import {
@@ -145,6 +143,7 @@ import {
145143
mountChildFibers,
146144
reconcileChildFibers,
147145
cloneChildFibers,
146+
validateSuspenseListChildren,
148147
} from './ReactChildFiber';
149148
import {
150149
processUpdateQueue,
@@ -3302,73 +3301,6 @@ function validateTailOptions(
33023301
}
33033302
}
33043303

3305-
function validateSuspenseListNestedChild(childSlot: mixed, index: number) {
3306-
if (__DEV__) {
3307-
const isAnArray = isArray(childSlot);
3308-
const isIterable =
3309-
!isAnArray && typeof getIteratorFn(childSlot) === 'function';
3310-
if (isAnArray || isIterable) {
3311-
const type = isAnArray ? 'array' : 'iterable';
3312-
console.error(
3313-
'A nested %s was passed to row #%s in <SuspenseList />. Wrap it in ' +
3314-
'an additional SuspenseList to configure its revealOrder: ' +
3315-
'<SuspenseList revealOrder=...> ... ' +
3316-
'<SuspenseList revealOrder=...>{%s}</SuspenseList> ... ' +
3317-
'</SuspenseList>',
3318-
type,
3319-
index,
3320-
type,
3321-
);
3322-
return false;
3323-
}
3324-
}
3325-
return true;
3326-
}
3327-
3328-
function validateSuspenseListChildren(
3329-
children: mixed,
3330-
revealOrder: SuspenseListRevealOrder,
3331-
) {
3332-
if (__DEV__) {
3333-
if (
3334-
(revealOrder === 'forwards' || revealOrder === 'backwards') &&
3335-
children !== undefined &&
3336-
children !== null &&
3337-
children !== false
3338-
) {
3339-
if (isArray(children)) {
3340-
for (let i = 0; i < children.length; i++) {
3341-
if (!validateSuspenseListNestedChild(children[i], i)) {
3342-
return;
3343-
}
3344-
}
3345-
} else {
3346-
const iteratorFn = getIteratorFn(children);
3347-
if (typeof iteratorFn === 'function') {
3348-
const childrenIterator = iteratorFn.call(children);
3349-
if (childrenIterator) {
3350-
let step = childrenIterator.next();
3351-
let i = 0;
3352-
for (; !step.done; step = childrenIterator.next()) {
3353-
if (!validateSuspenseListNestedChild(step.value, i)) {
3354-
return;
3355-
}
3356-
i++;
3357-
}
3358-
}
3359-
} else {
3360-
console.error(
3361-
'A single row was passed to a <SuspenseList revealOrder="%s" />. ' +
3362-
'This is not useful since it needs multiple rows. ' +
3363-
'Did you mean to pass multiple children or an array?',
3364-
revealOrder,
3365-
);
3366-
}
3367-
}
3368-
}
3369-
}
3370-
}
3371-
33723304
function initSuspenseListRenderState(
33733305
workInProgress: Fiber,
33743306
isBackwards: boolean,
@@ -3415,12 +3347,6 @@ function updateSuspenseListComponent(
34153347
const tailMode: SuspenseListTailMode = nextProps.tail;
34163348
const newChildren = nextProps.children;
34173349

3418-
validateRevealOrder(revealOrder);
3419-
validateTailOptions(tailMode, revealOrder);
3420-
validateSuspenseListChildren(newChildren, revealOrder);
3421-
3422-
reconcileChildren(current, workInProgress, newChildren, renderLanes);
3423-
34243350
let suspenseContext: SuspenseContext = suspenseStackCursor.current;
34253351

34263352
const shouldForceFallback = hasSuspenseListContext(
@@ -3434,6 +3360,17 @@ function updateSuspenseListComponent(
34343360
);
34353361
workInProgress.flags |= DidCapture;
34363362
} else {
3363+
suspenseContext = setDefaultShallowSuspenseListContext(suspenseContext);
3364+
}
3365+
pushSuspenseListContext(workInProgress, suspenseContext);
3366+
3367+
validateRevealOrder(revealOrder);
3368+
validateTailOptions(tailMode, revealOrder);
3369+
validateSuspenseListChildren(newChildren, revealOrder);
3370+
3371+
reconcileChildren(current, workInProgress, newChildren, renderLanes);
3372+
3373+
if (!shouldForceFallback) {
34373374
const didSuspendBefore =
34383375
current !== null && (current.flags & DidCapture) !== NoFlags;
34393376
if (didSuspendBefore) {
@@ -3446,9 +3383,7 @@ function updateSuspenseListComponent(
34463383
renderLanes,
34473384
);
34483385
}
3449-
suspenseContext = setDefaultShallowSuspenseListContext(suspenseContext);
34503386
}
3451-
pushSuspenseListContext(workInProgress, suspenseContext);
34523387

34533388
if (!disableLegacyMode && (workInProgress.mode & ConcurrentMode) === NoMode) {
34543389
// In legacy mode, SuspenseList doesn't work so we just

0 commit comments

Comments
 (0)