Unwrap a reference to a Lazy value (#34535)
If we are referencing a lazy value that isn't explicitly lazy ($L...) it's because we added it around an element that was blocked to be able to defer things inside. However, once that is unblocked we can start unwrap it and just use the inner element instead for any future reference. The race condition is still there since it's a race condition whether we added the wrapper in the first place. This just makes it consistent with unwrapping of the rest of the path.
Sebastian Markbåge committed
Sep 19, 2025 at 18:23 UTC
565eb7888efa5d8e70273d9b7a16374131348cbb
1 file changed
+69
-2
packages/react-client/src/ReactFlightClient.js
+69
-2
@@ -1337,7 +1337,11 @@ function fulfillReference(
1337
const {response, handler, parentObject, key, map, path} = reference;
1338
1339
for (let i = 1; i < path.length; i++) {
1340
- while (value.$$typeof === REACT_LAZY_TYPE) {
1340
+ while (
1341
+ typeof value === 'object' &&
1342
+ value !== null &&
1343
+ value.$$typeof === REACT_LAZY_TYPE
1344
+ ) {
1345
// We never expect to see a Lazy node on this path because we encode those as
1346
// separate models. This must mean that we have inserted an extra lazy node
1347
// e.g. to replace a blocked element. We must instead look for it inside.
@@ -1408,6 +1412,39 @@ function fulfillReference(
1412
}
1413
value = value[path[i]];
1414
}
1415
+
1416
+ while (
1417
+ typeof value === 'object' &&
1418
+ value !== null &&
1419
+ value.$$typeof === REACT_LAZY_TYPE
1420
+ ) {
1421
+ // If what we're referencing is a Lazy it must be because we inserted one as a virtual node
1422
+ // while it was blocked by other data. If it's no longer blocked, we can unwrap it.
1423
+ const referencedChunk: SomeChunk<any> = value._payload;
1424
+ if (referencedChunk === handler.chunk) {
1425
+ // This is a reference to the thing we're currently blocking. We can peak
1426
+ // inside of it to get the value.
1427
+ value = handler.value;
1428
+ continue;
1429
+ } else {
1430
+ switch (referencedChunk.status) {
1431
+ case RESOLVED_MODEL:
1432
+ initializeModelChunk(referencedChunk);
1433
+ break;
1434
+ case RESOLVED_MODULE:
1435
+ initializeModuleChunk(referencedChunk);
1436
+ break;
1437
+ }
1438
+ switch (referencedChunk.status) {
1439
+ case INITIALIZED: {
1440
+ value = referencedChunk.value;
1441
+ continue;
1442
+ }
1443
+ }
1444
+ }
1445
+ break;
1446
+ }
1447
+
1448
const mappedValue = map(response, value, parentObject, key);
1449
parentObject[key] = mappedValue;
1450
@@ -1855,7 +1892,11 @@ function getOutlinedModel<T>(
1892
case INITIALIZED:
1893
let value = chunk.value;
1894
for (let i = 1; i < path.length; i++) {
1858
- while (value.$$typeof === REACT_LAZY_TYPE) {
1895
+ while (
1896
+ typeof value === 'object' &&
1897
+ value !== null &&
1898
+ value.$$typeof === REACT_LAZY_TYPE
1899
+ ) {
1900
const referencedChunk: SomeChunk<any> = value._payload;
1901
switch (referencedChunk.status) {
1902
case RESOLVED_MODEL:
@@ -1924,6 +1965,32 @@ function getOutlinedModel<T>(
1965
}
1966
value = value[path[i]];
1967
}
1968
+
1969
+ while (
1970
+ typeof value === 'object' &&
1971
+ value !== null &&
1972
+ value.$$typeof === REACT_LAZY_TYPE
1973
+ ) {
1974
+ // If what we're referencing is a Lazy it must be because we inserted one as a virtual node
1975
+ // while it was blocked by other data. If it's no longer blocked, we can unwrap it.
1976
+ const referencedChunk: SomeChunk<any> = value._payload;
1977
+ switch (referencedChunk.status) {
1978
+ case RESOLVED_MODEL:
1979
+ initializeModelChunk(referencedChunk);
1980
+ break;
1981
+ case RESOLVED_MODULE:
1982
+ initializeModuleChunk(referencedChunk);
1983
+ break;
1984
+ }
1985
+ switch (referencedChunk.status) {
1986
+ case INITIALIZED: {
1987
+ value = referencedChunk.value;
1988
+ continue;
1989
+ }
1990
+ }
1991
+ break;
1992
+ }
1993
+
1994
const chunkValue = map(response, value, parentObject, key);
1995
if (
1996
parentObject[0] === REACT_ELEMENT_TYPE &&