Skip to content

Commit

Permalink
Suspend Thenable/Lazy if it's used in React.Children and unwrap (#28284)
Browse files Browse the repository at this point in the history
This pains me because `React.Children` is really already
pseudo-deprecated.

`React.Children` takes any children that `React.Node` takes. We now
support Lazy and Thenable in this position elsewhere, but it errors in
`React.Children`.

This becomes an issue with async Server Components which can resolve
into a Lazy and in the future Lazy will just become Thenables. Which
causes this to error.

There are a few different semantics we could have:

1) Error like we already do (#28280). `React.Children` is about
introspecting children. It was always sketchy because you can't
introspect inside an abstraction anyway. With Server Components we fold
away the components so you can actually introspect inside of them kind
of but what they do is an implementation detail and you should be able
to turn it into a Client Component at any point. The type of an Element
passing the boundary actually reduces to `React.Node`.
2) Suspend and unwrap the Node (this PR). If we assume that Children is
called inside of render, then throwing a Promise if it's not already
loaded or unwrapping would treat it as if it wasn't there. Just like if
you rendered it in React. This lets you introspect what's inside which
isn't really something you should be able to do. This isn't compatible
with deprecating throwing-a-Promise and enable static compilation like
`use()` does. We'd have to deprecate `React.Children` before doing that
which we might anyway.
3) Wrap in a Fragment. If a Server Component was instead a Client
Component, you couldn't introspect through it anyway. Another
alternative might be to let it pass through but then it wouldn't be
given a flat key. We could also wrap it in a Fragment that is keyed.
That way you're always seeing an element. The issue with this solution
is that it wouldn't see the key of the Server Component since that gets
forwarded to the child that is yet to resolve. The nice thing about that
strategy is it doesn't depend on throw-a-Promise but it might not be
keyed correctly when things move.

DiffTrain build for [9e7944f](9e7944f)
  • Loading branch information
sebmarkbage committed Feb 12, 2024
1 parent c4aeba9 commit 23457ab
Show file tree
Hide file tree
Showing 25 changed files with 816 additions and 410 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
629541bcc09fc7c0cc5c257541d084ee27457512
9e7944f67c72b9a69a8db092ba6bd99fe9c731e2
91 changes: 80 additions & 11 deletions compiled/facebook-www/React-dev.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ if (__DEV__) {
) {
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
}
var ReactVersion = "18.3.0-www-classic-98f29e71";
var ReactVersion = "18.3.0-www-classic-ab1e0612";

// ATTENTION
// When adding new symbols to this file,
Expand Down Expand Up @@ -2113,6 +2113,69 @@ if (__DEV__) {
return index.toString(36);
}

function noop$1() {}

function resolveThenable(thenable) {
switch (thenable.status) {
case "fulfilled": {
var fulfilledValue = thenable.value;
return fulfilledValue;
}

case "rejected": {
var rejectedError = thenable.reason;
throw rejectedError;
}

default: {
if (typeof thenable.status === "string") {
// Only instrument the thenable if the status if not defined. If
// it's defined, but an unknown value, assume it's been instrumented by
// some custom userspace implementation. We treat it as "pending".
// Attach a dummy listener, to ensure that any lazy initialization can
// happen. Flight lazily parses JSON when the value is actually awaited.
thenable.then(noop$1, noop$1);
} else {
// This is an uncached thenable that we haven't seen before.
// TODO: Detect infinite ping loops caused by uncached promises.
var pendingThenable = thenable;
pendingThenable.status = "pending";
pendingThenable.then(
function (fulfilledValue) {
if (thenable.status === "pending") {
var fulfilledThenable = thenable;
fulfilledThenable.status = "fulfilled";
fulfilledThenable.value = fulfilledValue;
}
},
function (error) {
if (thenable.status === "pending") {
var rejectedThenable = thenable;
rejectedThenable.status = "rejected";
rejectedThenable.reason = error;
}
}
);
} // Check one more time in case the thenable resolved synchronously.

switch (thenable.status) {
case "fulfilled": {
var fulfilledThenable = thenable;
return fulfilledThenable.value;
}

case "rejected": {
var rejectedThenable = thenable;
var _rejectedError = rejectedThenable.reason;
throw _rejectedError;
}
}
}
}

throw thenable;
}

function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
var type = typeof children;

Expand Down Expand Up @@ -2140,9 +2203,14 @@ if (__DEV__) {
break;

case REACT_LAZY_TYPE:
throw new Error(
"Cannot render an Async Component, Promise or React.Lazy inside React.Children. " +
"We recommend not iterating over children and just rendering them plain."
var payload = children._payload;
var init = children._init;
return mapIntoArray(
init(payload),
array,
escapedPrefix,
nameSoFar,
callback
);
}
}
Expand Down Expand Up @@ -2255,16 +2323,17 @@ if (__DEV__) {
);
}
} else if (type === "object") {
// eslint-disable-next-line react-internal/safe-string-coercion
var childrenString = String(children);

if (typeof children.then === "function") {
throw new Error(
"Cannot render an Async Component, Promise or React.Lazy inside React.Children. " +
"We recommend not iterating over children and just rendering them plain."
return mapIntoArray(
resolveThenable(children),
array,
escapedPrefix,
nameSoFar,
callback
);
}
} // eslint-disable-next-line react-internal/safe-string-coercion

var childrenString = String(children);
throw new Error(
"Objects are not valid as a React child (found: " +
(childrenString === "[object Object]"
Expand Down
91 changes: 80 additions & 11 deletions compiled/facebook-www/React-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ if (__DEV__) {
) {
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
}
var ReactVersion = "18.3.0-www-modern-33733854";
var ReactVersion = "18.3.0-www-modern-d9e56569";

// ATTENTION
// When adding new symbols to this file,
Expand Down Expand Up @@ -2078,6 +2078,69 @@ if (__DEV__) {
return index.toString(36);
}

function noop$1() {}

function resolveThenable(thenable) {
switch (thenable.status) {
case "fulfilled": {
var fulfilledValue = thenable.value;
return fulfilledValue;
}

case "rejected": {
var rejectedError = thenable.reason;
throw rejectedError;
}

default: {
if (typeof thenable.status === "string") {
// Only instrument the thenable if the status if not defined. If
// it's defined, but an unknown value, assume it's been instrumented by
// some custom userspace implementation. We treat it as "pending".
// Attach a dummy listener, to ensure that any lazy initialization can
// happen. Flight lazily parses JSON when the value is actually awaited.
thenable.then(noop$1, noop$1);
} else {
// This is an uncached thenable that we haven't seen before.
// TODO: Detect infinite ping loops caused by uncached promises.
var pendingThenable = thenable;
pendingThenable.status = "pending";
pendingThenable.then(
function (fulfilledValue) {
if (thenable.status === "pending") {
var fulfilledThenable = thenable;
fulfilledThenable.status = "fulfilled";
fulfilledThenable.value = fulfilledValue;
}
},
function (error) {
if (thenable.status === "pending") {
var rejectedThenable = thenable;
rejectedThenable.status = "rejected";
rejectedThenable.reason = error;
}
}
);
} // Check one more time in case the thenable resolved synchronously.

switch (thenable.status) {
case "fulfilled": {
var fulfilledThenable = thenable;
return fulfilledThenable.value;
}

case "rejected": {
var rejectedThenable = thenable;
var _rejectedError = rejectedThenable.reason;
throw _rejectedError;
}
}
}
}

throw thenable;
}

function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
var type = typeof children;

Expand Down Expand Up @@ -2105,9 +2168,14 @@ if (__DEV__) {
break;

case REACT_LAZY_TYPE:
throw new Error(
"Cannot render an Async Component, Promise or React.Lazy inside React.Children. " +
"We recommend not iterating over children and just rendering them plain."
var payload = children._payload;
var init = children._init;
return mapIntoArray(
init(payload),
array,
escapedPrefix,
nameSoFar,
callback
);
}
}
Expand Down Expand Up @@ -2220,16 +2288,17 @@ if (__DEV__) {
);
}
} else if (type === "object") {
// eslint-disable-next-line react-internal/safe-string-coercion
var childrenString = String(children);

if (typeof children.then === "function") {
throw new Error(
"Cannot render an Async Component, Promise or React.Lazy inside React.Children. " +
"We recommend not iterating over children and just rendering them plain."
return mapIntoArray(
resolveThenable(children),
array,
escapedPrefix,
nameSoFar,
callback
);
}
} // eslint-disable-next-line react-internal/safe-string-coercion

var childrenString = String(children);
throw new Error(
"Objects are not valid as a React child (found: " +
(childrenString === "[object Object]"
Expand Down
Loading

0 comments on commit 23457ab

Please sign in to comment.