Skip to content

Commit

Permalink
[Fiber/Fizz] Support AsyncIterable as Children and AsyncGenerator Cli…
Browse files Browse the repository at this point in the history
…ent Components (#28868)

Stacked on #28849, #28854, #28853. Behind a flag.

If you're following along from the side-lines. This is probably not what
you think it is.

It's NOT a way to get updates to a component over time. The
AsyncIterable works like an Iterable already works in React which is how
an Array works. I.e. it's a list of children - not the value of a child
over time.

It also doesn't actually render one component at a time. The way it
works is more like awaiting the entire list to become an array and then
it shows up. Before that it suspends the parent.

To actually get these to display one at a time, you have to opt-in with
`<SuspenseList>` to describe how they should appear. That's really the
interesting part and that not implemented yet.

Additionally, since these are effectively Async Functions and uncached
promises, they're not actually fully "supported" on the client yet for
the same reason rendering plain Promises and Async Functions aren't.
They warn. It's only really useful when paired with RSC that produces
instrumented versions of these. Ideally we'd published instrumented
helpers to help with map/filter style operations that yield new
instrumented AsyncIterables.

The way the implementation works basically just relies on unwrapThenable
and otherwise works like a plain Iterator.

There is one quirk with these that are different than just promises. We
ask for a new iterator each time we rerender. This means that upon retry
we kick off another iteration which itself might kick off new requests
that block iterating further. To solve this and make it actually
efficient enough to use on the client we'd need to stash something like
a buffer of the previous iteration and maybe iterator on the iterable so
that we can continue where we left off or synchronously iterate if we've
seen it before. Similar to our `.value` convention on Promises.

In Fizz, I had to do a special case because when we render an iterator
child we don't actually rerender the parent again like we do in Fiber.
However, it's more efficient to just continue on where we left off by
reusing the entries from the thenable state from before in that case.

DiffTrain build for [9f2eebd](9f2eebd)
  • Loading branch information
sebmarkbage committed Apr 22, 2024
1 parent d1d0508 commit f007da7
Show file tree
Hide file tree
Showing 28 changed files with 631 additions and 496 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3b551c82844bcfde51f0febb8e42c1a0d777df2c
9f2eebd807bf53b7d9901cf0b768762948224cae
22 changes: 14 additions & 8 deletions compiled/facebook-www/ReactART-dev.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function _assertThisInitialized(self) {
return self;
}

var ReactVersion = '19.0.0-www-classic-5033c724';
var ReactVersion = '19.0.0-www-classic-bd72d7f5';

var LegacyRoot = 0;
var ConcurrentRoot = 1;
Expand Down Expand Up @@ -169,6 +169,7 @@ var enableProfilerNestedUpdatePhase = true;
var enableAsyncActions = true;

var enableSchedulingProfiler = dynamicFeatureFlags.enableSchedulingProfiler;
var enableAsyncIterableChildren = false;
var disableLegacyMode = false;

var FunctionComponent = 0;
Expand Down Expand Up @@ -6597,7 +6598,7 @@ function createChildReconciler(shouldTrackSideEffects) {
}
}

if (isArray(newChild) || getIteratorFn(newChild)) {
if (isArray(newChild) || getIteratorFn(newChild) || enableAsyncIterableChildren ) {
var _created3 = createFiberFromFragment(newChild, returnFiber.mode, lanes, null);

_created3.return = returnFiber;
Expand Down Expand Up @@ -6682,7 +6683,7 @@ function createChildReconciler(shouldTrackSideEffects) {
}
}

if (isArray(newChild) || getIteratorFn(newChild)) {
if (isArray(newChild) || getIteratorFn(newChild) || enableAsyncIterableChildren ) {
if (key !== null) {
return null;
}
Expand Down Expand Up @@ -6750,7 +6751,7 @@ function createChildReconciler(shouldTrackSideEffects) {
return updateFromMap(existingChildren, returnFiber, newIdx, init(payload), lanes, mergeDebugInfo(debugInfo, newChild._debugInfo));
}

if (isArray(newChild) || getIteratorFn(newChild)) {
if (isArray(newChild) || getIteratorFn(newChild) || enableAsyncIterableChildren ) {
var _matchedFiber3 = existingChildren.get(newIdx) || null;

return updateFragment(returnFiber, _matchedFiber3, newChild, lanes, null, mergeDebugInfo(debugInfo, newChild._debugInfo));
Expand Down Expand Up @@ -6983,7 +6984,7 @@ function createChildReconciler(shouldTrackSideEffects) {
return resultingFirstChild;
}

function reconcileChildrenIterator(returnFiber, currentFirstChild, newChildrenIterable, lanes, debugInfo) {
function reconcileChildrenIteratable(returnFiber, currentFirstChild, newChildrenIterable, lanes, debugInfo) {
// This is the same implementation as reconcileChildrenArray(),
// but using the iterator instead.
var iteratorFn = getIteratorFn(newChildrenIterable);
Expand Down Expand Up @@ -7022,6 +7023,10 @@ function createChildReconciler(shouldTrackSideEffects) {
}
}

return reconcileChildrenIterator(returnFiber, currentFirstChild, newChildren, lanes, debugInfo);
}

function reconcileChildrenIterator(returnFiber, currentFirstChild, newChildren, lanes, debugInfo) {
if (newChildren == null) {
throw new Error('An iterable object provided no iterator.');
}
Expand Down Expand Up @@ -7327,8 +7332,8 @@ function createChildReconciler(shouldTrackSideEffects) {
}

if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, lanes, mergeDebugInfo(debugInfo, newChild._debugInfo));
} // Usables are a valid React node type. When React encounters a Usable in
return reconcileChildrenIteratable(returnFiber, currentFirstChild, newChild, lanes, mergeDebugInfo(debugInfo, newChild._debugInfo));
}
// a child position, it unwraps it using the same algorithm as `use`. For
// example, for promises, React will throw an exception to unwind the
// stack, then replay the component once the promise resolves.
Expand Down Expand Up @@ -7836,7 +7841,8 @@ function warnIfAsyncClientComponent(Component) {
// for transpiled async functions. Neither mechanism is completely
// bulletproof but together they cover the most common cases.
var isAsyncFunction = // $FlowIgnore[method-unbinding]
Object.prototype.toString.call(Component) === '[object AsyncFunction]';
Object.prototype.toString.call(Component) === '[object AsyncFunction]' || // $FlowIgnore[method-unbinding]
Object.prototype.toString.call(Component) === '[object AsyncGeneratorFunction]';

if (isAsyncFunction) {
// Encountered an async Client Component. This is not yet supported.
Expand Down
22 changes: 14 additions & 8 deletions compiled/facebook-www/ReactART-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function _assertThisInitialized(self) {
return self;
}

var ReactVersion = '19.0.0-www-modern-9c7c2ced';
var ReactVersion = '19.0.0-www-modern-03c6d7dd';

var LegacyRoot = 0;
var ConcurrentRoot = 1;
Expand Down Expand Up @@ -169,6 +169,7 @@ var enableProfilerNestedUpdatePhase = true;
var enableAsyncActions = true;

var enableSchedulingProfiler = dynamicFeatureFlags.enableSchedulingProfiler;
var enableAsyncIterableChildren = false;
var disableLegacyMode = true;

var FunctionComponent = 0;
Expand Down Expand Up @@ -6386,7 +6387,7 @@ function createChildReconciler(shouldTrackSideEffects) {
}
}

if (isArray(newChild) || getIteratorFn(newChild)) {
if (isArray(newChild) || getIteratorFn(newChild) || enableAsyncIterableChildren ) {
var _created3 = createFiberFromFragment(newChild, returnFiber.mode, lanes, null);

_created3.return = returnFiber;
Expand Down Expand Up @@ -6471,7 +6472,7 @@ function createChildReconciler(shouldTrackSideEffects) {
}
}

if (isArray(newChild) || getIteratorFn(newChild)) {
if (isArray(newChild) || getIteratorFn(newChild) || enableAsyncIterableChildren ) {
if (key !== null) {
return null;
}
Expand Down Expand Up @@ -6539,7 +6540,7 @@ function createChildReconciler(shouldTrackSideEffects) {
return updateFromMap(existingChildren, returnFiber, newIdx, init(payload), lanes, mergeDebugInfo(debugInfo, newChild._debugInfo));
}

if (isArray(newChild) || getIteratorFn(newChild)) {
if (isArray(newChild) || getIteratorFn(newChild) || enableAsyncIterableChildren ) {
var _matchedFiber3 = existingChildren.get(newIdx) || null;

return updateFragment(returnFiber, _matchedFiber3, newChild, lanes, null, mergeDebugInfo(debugInfo, newChild._debugInfo));
Expand Down Expand Up @@ -6772,7 +6773,7 @@ function createChildReconciler(shouldTrackSideEffects) {
return resultingFirstChild;
}

function reconcileChildrenIterator(returnFiber, currentFirstChild, newChildrenIterable, lanes, debugInfo) {
function reconcileChildrenIteratable(returnFiber, currentFirstChild, newChildrenIterable, lanes, debugInfo) {
// This is the same implementation as reconcileChildrenArray(),
// but using the iterator instead.
var iteratorFn = getIteratorFn(newChildrenIterable);
Expand Down Expand Up @@ -6811,6 +6812,10 @@ function createChildReconciler(shouldTrackSideEffects) {
}
}

return reconcileChildrenIterator(returnFiber, currentFirstChild, newChildren, lanes, debugInfo);
}

function reconcileChildrenIterator(returnFiber, currentFirstChild, newChildren, lanes, debugInfo) {
if (newChildren == null) {
throw new Error('An iterable object provided no iterator.');
}
Expand Down Expand Up @@ -7116,8 +7121,8 @@ function createChildReconciler(shouldTrackSideEffects) {
}

if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, lanes, mergeDebugInfo(debugInfo, newChild._debugInfo));
} // Usables are a valid React node type. When React encounters a Usable in
return reconcileChildrenIteratable(returnFiber, currentFirstChild, newChild, lanes, mergeDebugInfo(debugInfo, newChild._debugInfo));
}
// a child position, it unwraps it using the same algorithm as `use`. For
// example, for promises, React will throw an exception to unwind the
// stack, then replay the component once the promise resolves.
Expand Down Expand Up @@ -7625,7 +7630,8 @@ function warnIfAsyncClientComponent(Component) {
// for transpiled async functions. Neither mechanism is completely
// bulletproof but together they cover the most common cases.
var isAsyncFunction = // $FlowIgnore[method-unbinding]
Object.prototype.toString.call(Component) === '[object AsyncFunction]';
Object.prototype.toString.call(Component) === '[object AsyncFunction]' || // $FlowIgnore[method-unbinding]
Object.prototype.toString.call(Component) === '[object AsyncGeneratorFunction]';

if (isAsyncFunction) {
// Encountered an async Client Component. This is not yet supported.
Expand Down
70 changes: 37 additions & 33 deletions compiled/facebook-www/ReactART-prod.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -2204,22 +2204,19 @@ function createChildReconciler(shouldTrackSideEffects) {
function reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChildrenIterable,
newChildren,
lanes
) {
var iteratorFn = getIteratorFn(newChildrenIterable);
if ("function" !== typeof iteratorFn)
throw Error(formatProdErrorMessage(150));
newChildrenIterable = iteratorFn.call(newChildrenIterable);
if (null == newChildrenIterable) throw Error(formatProdErrorMessage(151));
if (null == newChildren) throw Error(formatProdErrorMessage(151));
for (
var previousNewFiber = (iteratorFn = null),
var resultingFirstChild = null,
previousNewFiber = null,
oldFiber = currentFirstChild,
newIdx = (currentFirstChild = 0),
nextOldFiber = null,
step = newChildrenIterable.next();
step = newChildren.next();
null !== oldFiber && !step.done;
newIdx++, step = newChildrenIterable.next(), null
newIdx++, step = newChildren.next(), null
) {
oldFiber.index > newIdx
? ((nextOldFiber = oldFiber), (oldFiber = null))
Expand All @@ -2235,28 +2232,30 @@ function createChildReconciler(shouldTrackSideEffects) {
deleteChild(returnFiber, oldFiber);
currentFirstChild = placeChild(newFiber, currentFirstChild, newIdx);
null === previousNewFiber
? (iteratorFn = newFiber)
? (resultingFirstChild = newFiber)
: (previousNewFiber.sibling = newFiber);
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (step.done)
return deleteRemainingChildren(returnFiber, oldFiber), iteratorFn;
return (
deleteRemainingChildren(returnFiber, oldFiber), resultingFirstChild
);
if (null === oldFiber) {
for (; !step.done; newIdx++, step = newChildrenIterable.next(), null)
for (; !step.done; newIdx++, step = newChildren.next(), null)
(step = createChild(returnFiber, step.value, lanes)),
null !== step &&
((currentFirstChild = placeChild(step, currentFirstChild, newIdx)),
null === previousNewFiber
? (iteratorFn = step)
? (resultingFirstChild = step)
: (previousNewFiber.sibling = step),
(previousNewFiber = step));
return iteratorFn;
return resultingFirstChild;
}
for (
oldFiber = mapRemainingChildren(oldFiber);
!step.done;
newIdx++, step = newChildrenIterable.next(), null
newIdx++, step = newChildren.next(), null
)
(step = updateFromMap(oldFiber, returnFiber, newIdx, step.value, lanes)),
null !== step &&
Expand All @@ -2265,14 +2264,14 @@ function createChildReconciler(shouldTrackSideEffects) {
oldFiber.delete(null === step.key ? newIdx : step.key),
(currentFirstChild = placeChild(step, currentFirstChild, newIdx)),
null === previousNewFiber
? (iteratorFn = step)
? (resultingFirstChild = step)
: (previousNewFiber.sibling = step),
(previousNewFiber = step));
shouldTrackSideEffects &&
oldFiber.forEach(function (child) {
return deleteChild(returnFiber, child);
});
return iteratorFn;
return resultingFirstChild;
}
function reconcileChildFibersImpl(
returnFiber,
Expand Down Expand Up @@ -2404,13 +2403,18 @@ function createChildReconciler(shouldTrackSideEffects) {
newChild,
lanes
);
if (getIteratorFn(newChild))
if (getIteratorFn(newChild)) {
child = getIteratorFn(newChild);
if ("function" !== typeof child)
throw Error(formatProdErrorMessage(150));
newChild = child.call(newChild);
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes
);
}
if ("function" === typeof newChild.then)
return reconcileChildFibersImpl(
returnFiber,
Expand Down Expand Up @@ -10619,19 +10623,19 @@ var slice = Array.prototype.slice,
};
return Text;
})(React.Component),
devToolsConfig$jscomp$inline_1114 = {
devToolsConfig$jscomp$inline_1123 = {
findFiberByHostInstance: function () {
return null;
},
bundleType: 0,
version: "19.0.0-www-classic-48f6e4f0",
version: "19.0.0-www-classic-6eb34c6f",
rendererPackageName: "react-art"
};
var internals$jscomp$inline_1322 = {
bundleType: devToolsConfig$jscomp$inline_1114.bundleType,
version: devToolsConfig$jscomp$inline_1114.version,
rendererPackageName: devToolsConfig$jscomp$inline_1114.rendererPackageName,
rendererConfig: devToolsConfig$jscomp$inline_1114.rendererConfig,
var internals$jscomp$inline_1331 = {
bundleType: devToolsConfig$jscomp$inline_1123.bundleType,
version: devToolsConfig$jscomp$inline_1123.version,
rendererPackageName: devToolsConfig$jscomp$inline_1123.rendererPackageName,
rendererConfig: devToolsConfig$jscomp$inline_1123.rendererConfig,
overrideHookState: null,
overrideHookStateDeletePath: null,
overrideHookStateRenamePath: null,
Expand All @@ -10648,26 +10652,26 @@ var internals$jscomp$inline_1322 = {
return null === fiber ? null : fiber.stateNode;
},
findFiberByHostInstance:
devToolsConfig$jscomp$inline_1114.findFiberByHostInstance ||
devToolsConfig$jscomp$inline_1123.findFiberByHostInstance ||
emptyFindFiberByHostInstance,
findHostInstancesForRefresh: null,
scheduleRefresh: null,
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "19.0.0-www-classic-48f6e4f0"
reconcilerVersion: "19.0.0-www-classic-6eb34c6f"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_1323 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
var hook$jscomp$inline_1332 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
if (
!hook$jscomp$inline_1323.isDisabled &&
hook$jscomp$inline_1323.supportsFiber
!hook$jscomp$inline_1332.isDisabled &&
hook$jscomp$inline_1332.supportsFiber
)
try {
(rendererID = hook$jscomp$inline_1323.inject(
internals$jscomp$inline_1322
(rendererID = hook$jscomp$inline_1332.inject(
internals$jscomp$inline_1331
)),
(injectedHook = hook$jscomp$inline_1323);
(injectedHook = hook$jscomp$inline_1332);
} catch (err) {}
}
var Path = Mode$1.Path;
Expand Down
Loading

0 comments on commit f007da7

Please sign in to comment.