Skip to content

Commit 1739b31

Browse files
committed
Suspend Thenable/Lazy if it's used in React.Children and unwrap (#28284)
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 commit 9e7944f.
1 parent 0bd6d70 commit 1739b31

File tree

13 files changed

+354
-207
lines changed

13 files changed

+354
-207
lines changed

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-dev.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<1aed0b993a4c164c11bd44255ec4ddac>>
10+
* @generated SignedSource<<9e5c6574214265b8a7f68fb3d0ee99e5>>
1111
*/
1212

1313
"use strict";
@@ -5021,20 +5021,20 @@ if (__DEV__) {
50215021
rejectedThenable.reason = error;
50225022
}
50235023
}
5024-
); // Check one more time in case the thenable resolved synchronously.
5024+
);
5025+
} // Check one more time in case the thenable resolved synchronously.
50255026

5026-
switch (thenable.status) {
5027-
case "fulfilled": {
5028-
var fulfilledThenable = thenable;
5029-
return fulfilledThenable.value;
5030-
}
5027+
switch (thenable.status) {
5028+
case "fulfilled": {
5029+
var fulfilledThenable = thenable;
5030+
return fulfilledThenable.value;
5031+
}
50315032

5032-
case "rejected": {
5033-
var rejectedThenable = thenable;
5034-
var _rejectedError = rejectedThenable.reason;
5035-
checkIfUseWrappedInAsyncCatch(_rejectedError);
5036-
throw _rejectedError;
5037-
}
5033+
case "rejected": {
5034+
var rejectedThenable = thenable;
5035+
var _rejectedError = rejectedThenable.reason;
5036+
checkIfUseWrappedInAsyncCatch(_rejectedError);
5037+
throw _rejectedError;
50385038
}
50395039
} // Suspend.
50405040
//
@@ -25721,7 +25721,7 @@ if (__DEV__) {
2572125721
return root;
2572225722
}
2572325723

25724-
var ReactVersion = "18.3.0-canary-629541bcc-20240212";
25724+
var ReactVersion = "18.3.0-canary-9e7944f67-20240212";
2572525725

2572625726
// Might add PROFILE later.
2572725727

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-prod.js

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<1fafe8c779f4832b19959f28b49afe3e>>
10+
* @generated SignedSource<<6223b3402ebe91d202065e784f2d9af4>>
1111
*/
1212

1313
"use strict";
@@ -1402,16 +1402,16 @@ function trackUsedThenable(thenableState, thenable, index) {
14021402
}
14031403
}
14041404
);
1405-
switch (thenable.status) {
1406-
case "fulfilled":
1407-
return thenable.value;
1408-
case "rejected":
1409-
throw (
1410-
((thenableState = thenable.reason),
1411-
checkIfUseWrappedInAsyncCatch(thenableState),
1412-
thenableState)
1413-
);
1414-
}
1405+
}
1406+
switch (thenable.status) {
1407+
case "fulfilled":
1408+
return thenable.value;
1409+
case "rejected":
1410+
throw (
1411+
((thenableState = thenable.reason),
1412+
checkIfUseWrappedInAsyncCatch(thenableState),
1413+
thenableState)
1414+
);
14151415
}
14161416
suspendedThenable = thenable;
14171417
throw SuspenseException;
@@ -9173,7 +9173,7 @@ var devToolsConfig$jscomp$inline_1023 = {
91739173
throw Error("TestRenderer does not support findFiberByHostInstance()");
91749174
},
91759175
bundleType: 0,
9176-
version: "18.3.0-canary-629541bcc-20240212",
9176+
version: "18.3.0-canary-9e7944f67-20240212",
91779177
rendererPackageName: "react-test-renderer"
91789178
};
91799179
var internals$jscomp$inline_1204 = {
@@ -9204,7 +9204,7 @@ var internals$jscomp$inline_1204 = {
92049204
scheduleRoot: null,
92059205
setRefreshHandler: null,
92069206
getCurrentFiber: null,
9207-
reconcilerVersion: "18.3.0-canary-629541bcc-20240212"
9207+
reconcilerVersion: "18.3.0-canary-9e7944f67-20240212"
92089208
};
92099209
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
92109210
var hook$jscomp$inline_1205 = __REACT_DEVTOOLS_GLOBAL_HOOK__;

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-profiling.js

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<a0a2d876cbe4fb943cdc49c02ecf6a31>>
10+
* @generated SignedSource<<9a90acaf1cc6a5a23ed59dfb8d38c84f>>
1111
*/
1212

1313
"use strict";
@@ -1422,16 +1422,16 @@ function trackUsedThenable(thenableState, thenable, index) {
14221422
}
14231423
}
14241424
);
1425-
switch (thenable.status) {
1426-
case "fulfilled":
1427-
return thenable.value;
1428-
case "rejected":
1429-
throw (
1430-
((thenableState = thenable.reason),
1431-
checkIfUseWrappedInAsyncCatch(thenableState),
1432-
thenableState)
1433-
);
1434-
}
1425+
}
1426+
switch (thenable.status) {
1427+
case "fulfilled":
1428+
return thenable.value;
1429+
case "rejected":
1430+
throw (
1431+
((thenableState = thenable.reason),
1432+
checkIfUseWrappedInAsyncCatch(thenableState),
1433+
thenableState)
1434+
);
14351435
}
14361436
suspendedThenable = thenable;
14371437
throw SuspenseException;
@@ -9601,7 +9601,7 @@ var devToolsConfig$jscomp$inline_1065 = {
96019601
throw Error("TestRenderer does not support findFiberByHostInstance()");
96029602
},
96039603
bundleType: 0,
9604-
version: "18.3.0-canary-629541bcc-20240212",
9604+
version: "18.3.0-canary-9e7944f67-20240212",
96059605
rendererPackageName: "react-test-renderer"
96069606
};
96079607
var internals$jscomp$inline_1245 = {
@@ -9632,7 +9632,7 @@ var internals$jscomp$inline_1245 = {
96329632
scheduleRoot: null,
96339633
setRefreshHandler: null,
96349634
getCurrentFiber: null,
9635-
reconcilerVersion: "18.3.0-canary-629541bcc-20240212"
9635+
reconcilerVersion: "18.3.0-canary-9e7944f67-20240212"
96369636
};
96379637
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
96389638
var hook$jscomp$inline_1246 = __REACT_DEVTOOLS_GLOBAL_HOOK__;

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/cjs/React-dev.js

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<70ab383eec7c39690b53a887a16d26d0>>
10+
* @generated SignedSource<<e3defaed524124e7d1cb2b9785a29b43>>
1111
*/
1212

1313
"use strict";
@@ -24,7 +24,7 @@ if (__DEV__) {
2424
) {
2525
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
2626
}
27-
var ReactVersion = "18.3.0-canary-629541bcc-20240212";
27+
var ReactVersion = "18.3.0-canary-9e7944f67-20240212";
2828

2929
// ATTENTION
3030
// When adding new symbols to this file,
@@ -1768,6 +1768,69 @@ if (__DEV__) {
17681768
return index.toString(36);
17691769
}
17701770

1771+
function noop() {}
1772+
1773+
function resolveThenable(thenable) {
1774+
switch (thenable.status) {
1775+
case "fulfilled": {
1776+
var fulfilledValue = thenable.value;
1777+
return fulfilledValue;
1778+
}
1779+
1780+
case "rejected": {
1781+
var rejectedError = thenable.reason;
1782+
throw rejectedError;
1783+
}
1784+
1785+
default: {
1786+
if (typeof thenable.status === "string") {
1787+
// Only instrument the thenable if the status if not defined. If
1788+
// it's defined, but an unknown value, assume it's been instrumented by
1789+
// some custom userspace implementation. We treat it as "pending".
1790+
// Attach a dummy listener, to ensure that any lazy initialization can
1791+
// happen. Flight lazily parses JSON when the value is actually awaited.
1792+
thenable.then(noop, noop);
1793+
} else {
1794+
// This is an uncached thenable that we haven't seen before.
1795+
// TODO: Detect infinite ping loops caused by uncached promises.
1796+
var pendingThenable = thenable;
1797+
pendingThenable.status = "pending";
1798+
pendingThenable.then(
1799+
function (fulfilledValue) {
1800+
if (thenable.status === "pending") {
1801+
var fulfilledThenable = thenable;
1802+
fulfilledThenable.status = "fulfilled";
1803+
fulfilledThenable.value = fulfilledValue;
1804+
}
1805+
},
1806+
function (error) {
1807+
if (thenable.status === "pending") {
1808+
var rejectedThenable = thenable;
1809+
rejectedThenable.status = "rejected";
1810+
rejectedThenable.reason = error;
1811+
}
1812+
}
1813+
);
1814+
} // Check one more time in case the thenable resolved synchronously.
1815+
1816+
switch (thenable.status) {
1817+
case "fulfilled": {
1818+
var fulfilledThenable = thenable;
1819+
return fulfilledThenable.value;
1820+
}
1821+
1822+
case "rejected": {
1823+
var rejectedThenable = thenable;
1824+
var _rejectedError = rejectedThenable.reason;
1825+
throw _rejectedError;
1826+
}
1827+
}
1828+
}
1829+
}
1830+
1831+
throw thenable;
1832+
}
1833+
17711834
function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
17721835
var type = typeof children;
17731836

@@ -1795,9 +1858,14 @@ if (__DEV__) {
17951858
break;
17961859

17971860
case REACT_LAZY_TYPE:
1798-
throw new Error(
1799-
"Cannot render an Async Component, Promise or React.Lazy inside React.Children. " +
1800-
"We recommend not iterating over children and just rendering them plain."
1861+
var payload = children._payload;
1862+
var init = children._init;
1863+
return mapIntoArray(
1864+
init(payload),
1865+
array,
1866+
escapedPrefix,
1867+
nameSoFar,
1868+
callback
18011869
);
18021870
}
18031871
}
@@ -1910,16 +1978,17 @@ if (__DEV__) {
19101978
);
19111979
}
19121980
} else if (type === "object") {
1913-
// eslint-disable-next-line react-internal/safe-string-coercion
1914-
var childrenString = String(children);
1915-
19161981
if (typeof children.then === "function") {
1917-
throw new Error(
1918-
"Cannot render an Async Component, Promise or React.Lazy inside React.Children. " +
1919-
"We recommend not iterating over children and just rendering them plain."
1982+
return mapIntoArray(
1983+
resolveThenable(children),
1984+
array,
1985+
escapedPrefix,
1986+
nameSoFar,
1987+
callback
19201988
);
1921-
}
1989+
} // eslint-disable-next-line react-internal/safe-string-coercion
19221990

1991+
var childrenString = String(children);
19231992
throw new Error(
19241993
"Objects are not valid as a React child (found: " +
19251994
(childrenString === "[object Object]"

0 commit comments

Comments
 (0)