Skip to content

Commit c6d3f9a

Browse files
committed
Bugfix: Selective hydration triggers false update loop error (#27439)
This adds a regression test and fix for a case where a sync update triggers selective hydration, which then leads to a "Maximum update depth exceeded" error, even though there was only a single update. This happens when a single sync update flows into many sibling dehydrated Suspense boundaries. This fix is, if a commit was the result of selective hydration, we should not increment the nested update count, because those renders conceptually are not updates. Ideally, they wouldn't even be in a separate commit — we should be able to hydrate a tree and apply an update on top of it within the same render phase. We could do this once we implement resumable context stacks. DiffTrain build for [d900fad](d900fad)
1 parent ab2c406 commit c6d3f9a

23 files changed

Lines changed: 249 additions & 149 deletions

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
13d0225c7d4715d98772a85d8deb26d244921287
1+
d900fadbf9017063fecb2641b7e99303b82a6f17

compiled/facebook-www/React-dev.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ if (
2727
}
2828
"use strict";
2929

30-
var ReactVersion = "18.3.0-www-classic-f8c6554b";
30+
var ReactVersion = "18.3.0-www-classic-d42cdbcf";
3131

3232
// ATTENTION
3333
// When adding new symbols to this file,

compiled/facebook-www/React-prod.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,4 +623,4 @@ exports.useSyncExternalStore = function (
623623
exports.useTransition = function () {
624624
return ReactCurrentDispatcher.current.useTransition();
625625
};
626-
exports.version = "18.3.0-www-classic-c257e462";
626+
exports.version = "18.3.0-www-classic-f8e56db8";

compiled/facebook-www/React-prod.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,4 +615,4 @@ exports.useSyncExternalStore = function (
615615
exports.useTransition = function () {
616616
return ReactCurrentDispatcher.current.useTransition();
617617
};
618-
exports.version = "18.3.0-www-modern-1f62d21d";
618+
exports.version = "18.3.0-www-modern-34d9e517";

compiled/facebook-www/React-profiling.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,7 @@ exports.useSyncExternalStore = function (
626626
exports.useTransition = function () {
627627
return ReactCurrentDispatcher.current.useTransition();
628628
};
629-
exports.version = "18.3.0-www-modern-9df16c08";
629+
exports.version = "18.3.0-www-modern-a77dd006";
630630

631631
/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
632632
if (

compiled/facebook-www/ReactART-dev.classic.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function _assertThisInitialized(self) {
6969
return self;
7070
}
7171

72-
var ReactVersion = "18.3.0-www-classic-bface95b";
72+
var ReactVersion = "18.3.0-www-classic-6b865fa6";
7373

7474
var LegacyRoot = 0;
7575
var ConcurrentRoot = 1;
@@ -1588,9 +1588,9 @@ var DefaultHydrationLane =
15881588
var DefaultLane =
15891589
/* */
15901590
32;
1591-
var SyncUpdateLanes =
1592-
/* */
1593-
42;
1591+
var SyncUpdateLanes = enableUnifiedSyncLane
1592+
? SyncLane | InputContinuousLane | DefaultLane
1593+
: SyncLane;
15941594
var TransitionHydrationLane =
15951595
/* */
15961596
64;
@@ -1675,7 +1675,11 @@ var IdleLane =
16751675
536870912;
16761676
var OffscreenLane =
16771677
/* */
1678-
1073741824; // This function is used for the experimental timeline (react-devtools-timeline)
1678+
1073741824; // Any lane that might schedule an update. This is used to detect infinite
1679+
// update loops, so it doesn't include hydration lanes or retries.
1680+
1681+
var UpdateLanes =
1682+
SyncLane | InputContinuousLane | DefaultLane | TransitionLanes; // This function is used for the experimental timeline (react-devtools-timeline)
16791683
// It should be kept in sync with the Lanes values above.
16801684

16811685
function getLabelForLane(lane) {
@@ -26165,9 +26169,16 @@ function commitRootImpl(
2616526169
flushPassiveEffects();
2616626170
} // Read this again, since a passive effect might have updated it
2616726171

26168-
remainingLanes = root.pendingLanes;
26172+
remainingLanes = root.pendingLanes; // Check if this render scheduled a cascading synchronous update. This is a
26173+
// heurstic to detect infinite update loops. We are intentionally excluding
26174+
// hydration lanes in this check, because render triggered by selective
26175+
// hydration is conceptually not an update.
2616926176

26170-
if (includesSyncLane(remainingLanes)) {
26177+
if (
26178+
// Was the finished render the result of an update (not hydration)?
26179+
includesSomeLane(lanes, UpdateLanes) && // Did it schedule a sync update?
26180+
includesSomeLane(remainingLanes, SyncUpdateLanes)
26181+
) {
2617126182
{
2617226183
markNestedUpdateScheduled();
2617326184
} // Count the number of times the root synchronously re-renders without

compiled/facebook-www/ReactART-dev.modern.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function _assertThisInitialized(self) {
6969
return self;
7070
}
7171

72-
var ReactVersion = "18.3.0-www-modern-87c144fd";
72+
var ReactVersion = "18.3.0-www-modern-10178652";
7373

7474
var LegacyRoot = 0;
7575
var ConcurrentRoot = 1;
@@ -1585,9 +1585,9 @@ var DefaultHydrationLane =
15851585
var DefaultLane =
15861586
/* */
15871587
32;
1588-
var SyncUpdateLanes =
1589-
/* */
1590-
42;
1588+
var SyncUpdateLanes = enableUnifiedSyncLane
1589+
? SyncLane | InputContinuousLane | DefaultLane
1590+
: SyncLane;
15911591
var TransitionHydrationLane =
15921592
/* */
15931593
64;
@@ -1672,7 +1672,11 @@ var IdleLane =
16721672
536870912;
16731673
var OffscreenLane =
16741674
/* */
1675-
1073741824; // This function is used for the experimental timeline (react-devtools-timeline)
1675+
1073741824; // Any lane that might schedule an update. This is used to detect infinite
1676+
// update loops, so it doesn't include hydration lanes or retries.
1677+
1678+
var UpdateLanes =
1679+
SyncLane | InputContinuousLane | DefaultLane | TransitionLanes; // This function is used for the experimental timeline (react-devtools-timeline)
16761680
// It should be kept in sync with the Lanes values above.
16771681

16781682
function getLabelForLane(lane) {
@@ -25825,9 +25829,16 @@ function commitRootImpl(
2582525829
flushPassiveEffects();
2582625830
} // Read this again, since a passive effect might have updated it
2582725831

25828-
remainingLanes = root.pendingLanes;
25832+
remainingLanes = root.pendingLanes; // Check if this render scheduled a cascading synchronous update. This is a
25833+
// heurstic to detect infinite update loops. We are intentionally excluding
25834+
// hydration lanes in this check, because render triggered by selective
25835+
// hydration is conceptually not an update.
2582925836

25830-
if (includesSyncLane(remainingLanes)) {
25837+
if (
25838+
// Was the finished render the result of an update (not hydration)?
25839+
includesSomeLane(lanes, UpdateLanes) && // Did it schedule a sync update?
25840+
includesSomeLane(remainingLanes, SyncUpdateLanes)
25841+
) {
2583125842
{
2583225843
markNestedUpdateScheduled();
2583325844
} // Count the number of times the root synchronously re-renders without

compiled/facebook-www/ReactART-prod.classic.js

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -393,11 +393,12 @@ function clz32Fallback(x) {
393393
x >>>= 0;
394394
return 0 === x ? 32 : (31 - ((log(x) / LN2) | 0)) | 0;
395395
}
396-
var nextTransitionLane = 128,
396+
var SyncUpdateLanes = enableUnifiedSyncLane ? 42 : 2,
397+
nextTransitionLane = 128,
397398
nextRetryLane = 8388608;
398399
function getHighestPriorityLanes(lanes) {
399400
if (enableUnifiedSyncLane) {
400-
var pendingSyncLanes = lanes & 42;
401+
var pendingSyncLanes = lanes & SyncUpdateLanes;
401402
if (0 !== pendingSyncLanes) return pendingSyncLanes;
402403
}
403404
switch (lanes & -lanes) {
@@ -4812,7 +4813,8 @@ function updateDehydratedSuspenseComponent(
48124813
nextProps = workInProgressRoot;
48134814
if (null !== nextProps) {
48144815
didSuspend = renderLanes & -renderLanes;
4815-
if (enableUnifiedSyncLane && 0 !== (didSuspend & 42)) didSuspend = 1;
4816+
if (enableUnifiedSyncLane && 0 !== (didSuspend & SyncUpdateLanes))
4817+
didSuspend = 1;
48164818
else
48174819
switch (didSuspend) {
48184820
case 2:
@@ -8945,12 +8947,12 @@ function commitRootImpl(
89458947
finishedWork < recoverableErrors.length;
89468948
finishedWork++
89478949
)
8948-
(lanes = recoverableErrors[finishedWork]),
8949-
(remainingLanes = {
8950-
digest: lanes.digest,
8951-
componentStack: lanes.stack
8950+
(remainingLanes = recoverableErrors[finishedWork]),
8951+
(transitions = {
8952+
digest: remainingLanes.digest,
8953+
componentStack: remainingLanes.stack
89528954
}),
8953-
renderPriorityLevel(lanes.value, remainingLanes);
8955+
renderPriorityLevel(remainingLanes.value, transitions);
89548956
if (hasUncaughtError)
89558957
throw (
89568958
((hasUncaughtError = !1),
@@ -8962,7 +8964,7 @@ function commitRootImpl(
89628964
0 !== root.tag &&
89638965
flushPassiveEffects();
89648966
remainingLanes = root.pendingLanes;
8965-
0 !== (remainingLanes & 3)
8967+
0 !== (lanes & 8388522) && 0 !== (remainingLanes & SyncUpdateLanes)
89668968
? root === rootWithNestedUpdates
89678969
? nestedUpdateCount++
89688970
: ((nestedUpdateCount = 0), (rootWithNestedUpdates = root))
@@ -10105,7 +10107,7 @@ var slice = Array.prototype.slice,
1010510107
return null;
1010610108
},
1010710109
bundleType: 0,
10108-
version: "18.3.0-www-classic-b3975372",
10110+
version: "18.3.0-www-classic-2b1e1230",
1010910111
rendererPackageName: "react-art"
1011010112
};
1011110113
var internals$jscomp$inline_1304 = {
@@ -10136,7 +10138,7 @@ var internals$jscomp$inline_1304 = {
1013610138
scheduleRoot: null,
1013710139
setRefreshHandler: null,
1013810140
getCurrentFiber: null,
10139-
reconcilerVersion: "18.3.0-www-classic-b3975372"
10141+
reconcilerVersion: "18.3.0-www-classic-2b1e1230"
1014010142
};
1014110143
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
1014210144
var hook$jscomp$inline_1305 = __REACT_DEVTOOLS_GLOBAL_HOOK__;

compiled/facebook-www/ReactART-prod.modern.js

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -277,11 +277,12 @@ function clz32Fallback(x) {
277277
x >>>= 0;
278278
return 0 === x ? 32 : (31 - ((log(x) / LN2) | 0)) | 0;
279279
}
280-
var nextTransitionLane = 128,
280+
var SyncUpdateLanes = enableUnifiedSyncLane ? 42 : 2,
281+
nextTransitionLane = 128,
281282
nextRetryLane = 8388608;
282283
function getHighestPriorityLanes(lanes) {
283284
if (enableUnifiedSyncLane) {
284-
var pendingSyncLanes = lanes & 42;
285+
var pendingSyncLanes = lanes & SyncUpdateLanes;
285286
if (0 !== pendingSyncLanes) return pendingSyncLanes;
286287
}
287288
switch (lanes & -lanes) {
@@ -4571,7 +4572,8 @@ function updateDehydratedSuspenseComponent(
45714572
nextProps = workInProgressRoot;
45724573
if (null !== nextProps) {
45734574
didSuspend = renderLanes & -renderLanes;
4574-
if (enableUnifiedSyncLane && 0 !== (didSuspend & 42)) didSuspend = 1;
4575+
if (enableUnifiedSyncLane && 0 !== (didSuspend & SyncUpdateLanes))
4576+
didSuspend = 1;
45754577
else
45764578
switch (didSuspend) {
45774579
case 2:
@@ -8677,12 +8679,12 @@ function commitRootImpl(
86778679
finishedWork < recoverableErrors.length;
86788680
finishedWork++
86798681
)
8680-
(lanes = recoverableErrors[finishedWork]),
8681-
(remainingLanes = {
8682-
digest: lanes.digest,
8683-
componentStack: lanes.stack
8682+
(remainingLanes = recoverableErrors[finishedWork]),
8683+
(transitions = {
8684+
digest: remainingLanes.digest,
8685+
componentStack: remainingLanes.stack
86848686
}),
8685-
renderPriorityLevel(lanes.value, remainingLanes);
8687+
renderPriorityLevel(remainingLanes.value, transitions);
86868688
if (hasUncaughtError)
86878689
throw (
86888690
((hasUncaughtError = !1),
@@ -8694,7 +8696,7 @@ function commitRootImpl(
86948696
0 !== root.tag &&
86958697
flushPassiveEffects();
86968698
remainingLanes = root.pendingLanes;
8697-
0 !== (remainingLanes & 3)
8699+
0 !== (lanes & 8388522) && 0 !== (remainingLanes & SyncUpdateLanes)
86988700
? root === rootWithNestedUpdates
86998701
? nestedUpdateCount++
87008702
: ((nestedUpdateCount = 0), (rootWithNestedUpdates = root))
@@ -9770,7 +9772,7 @@ var slice = Array.prototype.slice,
97709772
return null;
97719773
},
97729774
bundleType: 0,
9773-
version: "18.3.0-www-modern-a4e8bc7c",
9775+
version: "18.3.0-www-modern-df7dbab9",
97749776
rendererPackageName: "react-art"
97759777
};
97769778
var internals$jscomp$inline_1284 = {
@@ -9801,7 +9803,7 @@ var internals$jscomp$inline_1284 = {
98019803
scheduleRoot: null,
98029804
setRefreshHandler: null,
98039805
getCurrentFiber: null,
9804-
reconcilerVersion: "18.3.0-www-modern-a4e8bc7c"
9806+
reconcilerVersion: "18.3.0-www-modern-df7dbab9"
98059807
};
98069808
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
98079809
var hook$jscomp$inline_1285 = __REACT_DEVTOOLS_GLOBAL_HOOK__;

compiled/facebook-www/ReactDOM-dev.classic.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1758,9 +1758,9 @@ var DefaultHydrationLane =
17581758
var DefaultLane =
17591759
/* */
17601760
32;
1761-
var SyncUpdateLanes =
1762-
/* */
1763-
42;
1761+
var SyncUpdateLanes = enableUnifiedSyncLane
1762+
? SyncLane | InputContinuousLane | DefaultLane
1763+
: SyncLane;
17641764
var TransitionHydrationLane =
17651765
/* */
17661766
64;
@@ -1845,7 +1845,11 @@ var IdleLane =
18451845
536870912;
18461846
var OffscreenLane =
18471847
/* */
1848-
1073741824; // This function is used for the experimental timeline (react-devtools-timeline)
1848+
1073741824; // Any lane that might schedule an update. This is used to detect infinite
1849+
// update loops, so it doesn't include hydration lanes or retries.
1850+
1851+
var UpdateLanes =
1852+
SyncLane | InputContinuousLane | DefaultLane | TransitionLanes; // This function is used for the experimental timeline (react-devtools-timeline)
18491853
// It should be kept in sync with the Lanes values above.
18501854

18511855
function getLabelForLane(lane) {
@@ -31705,9 +31709,16 @@ function commitRootImpl(
3170531709
flushPassiveEffects();
3170631710
} // Read this again, since a passive effect might have updated it
3170731711

31708-
remainingLanes = root.pendingLanes;
31712+
remainingLanes = root.pendingLanes; // Check if this render scheduled a cascading synchronous update. This is a
31713+
// heurstic to detect infinite update loops. We are intentionally excluding
31714+
// hydration lanes in this check, because render triggered by selective
31715+
// hydration is conceptually not an update.
3170931716

31710-
if (includesSyncLane(remainingLanes)) {
31717+
if (
31718+
// Was the finished render the result of an update (not hydration)?
31719+
includesSomeLane(lanes, UpdateLanes) && // Did it schedule a sync update?
31720+
includesSomeLane(remainingLanes, SyncUpdateLanes)
31721+
) {
3171131722
{
3171231723
markNestedUpdateScheduled();
3171331724
} // Count the number of times the root synchronously re-renders without
@@ -33961,7 +33972,7 @@ function createFiberRoot(
3396133972
return root;
3396233973
}
3396333974

33964-
var ReactVersion = "18.3.0-www-classic-babee8fb";
33975+
var ReactVersion = "18.3.0-www-classic-707e543f";
3396533976

3396633977
function createPortal$1(
3396733978
children,

0 commit comments

Comments
 (0)