Skip to content

Commit de68d2f

Browse files
authored
Register Suspense retry handlers in commit phase (#31667)
To avoid GC pressure and accidentally hanging onto old trees Suspense boundary retries are now implemented in the commit phase. I used the Callback flag which was previously only used to schedule callbacks for Class components. This isn't quite semantically equivalent but it's unused and seemingly compatible.
1 parent 16d2bbb commit de68d2f

File tree

3 files changed

+43
-19
lines changed

3 files changed

+43
-19
lines changed

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

+21-13
Original file line numberDiff line numberDiff line change
@@ -1310,20 +1310,28 @@ export function registerSuspenseInstanceRetry(
13101310
callback: () => void,
13111311
) {
13121312
const ownerDocument = instance.ownerDocument;
1313-
if (ownerDocument.readyState !== DOCUMENT_READY_STATE_COMPLETE) {
1314-
ownerDocument.addEventListener(
1315-
'DOMContentLoaded',
1316-
() => {
1317-
if (instance.data === SUSPENSE_PENDING_START_DATA) {
1318-
callback();
1319-
}
1320-
},
1321-
{
1322-
once: true,
1323-
},
1324-
);
1313+
if (
1314+
// The Fizz runtime must have put this boundary into client render or complete
1315+
// state after the render finished but before it committed. We need to call the
1316+
// callback now rather than wait
1317+
instance.data !== SUSPENSE_PENDING_START_DATA ||
1318+
// The boundary is still in pending status but the document has finished loading
1319+
// before we could register the event handler that would have scheduled the retry
1320+
// on load so we call teh callback now.
1321+
ownerDocument.readyState === DOCUMENT_READY_STATE_COMPLETE
1322+
) {
1323+
callback();
1324+
} else {
1325+
// We're still in pending status and the document is still loading so we attach
1326+
// a listener to the document load even and expose the retry on the instance for
1327+
// the Fizz runtime to trigger if it ends up resolving this boundary
1328+
const listener = () => {
1329+
callback();
1330+
ownerDocument.removeEventListener('DOMContentLoaded', listener);
1331+
};
1332+
ownerDocument.addEventListener('DOMContentLoaded', listener);
1333+
instance._reactRetry = listener;
13251334
}
1326-
instance._reactRetry = callback;
13271335
}
13281336

13291337
export function canHydrateFormStateMarker(

packages/react-reconciler/src/ReactFiberBeginWork.js

+3-6
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import {
7979
PerformedWork,
8080
Placement,
8181
Hydrating,
82+
Callback,
8283
ContentReset,
8384
DidCapture,
8485
Update,
@@ -166,7 +167,6 @@ import {
166167
isSuspenseInstancePending,
167168
isSuspenseInstanceFallback,
168169
getSuspenseInstanceFallbackErrorDetails,
169-
registerSuspenseInstanceRetry,
170170
supportsHydration,
171171
supportsResources,
172172
supportsSingletons,
@@ -256,7 +256,6 @@ import {
256256
isFunctionClassComponent,
257257
} from './ReactFiber';
258258
import {
259-
retryDehydratedSuspenseBoundary,
260259
scheduleUpdateOnFiber,
261260
renderDidSuspendDelayIfPossible,
262261
markSkippedUpdateLanes,
@@ -2816,12 +2815,10 @@ function updateDehydratedSuspenseComponent(
28162815
// on the client than if we just leave it alone. If the server times out or errors
28172816
// these should update this boundary to the permanent Fallback state instead.
28182817
// Mark it as having captured (i.e. suspended).
2819-
workInProgress.flags |= DidCapture;
2818+
// Also Mark it as requiring retry.
2819+
workInProgress.flags |= DidCapture | Callback;
28202820
// Leave the child in place. I.e. the dehydrated fragment.
28212821
workInProgress.child = current.child;
2822-
// Register a callback to retry this boundary once the server has sent the result.
2823-
const retry = retryDehydratedSuspenseBoundary.bind(null, current);
2824-
registerSuspenseInstanceRetry(suspenseInstance, retry);
28252822
return null;
28262823
} else {
28272824
// This is the first attempt.

packages/react-reconciler/src/ReactFiberCommitWork.js

+19
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ import {
142142
suspendInstance,
143143
suspendResource,
144144
resetFormInstance,
145+
registerSuspenseInstanceRetry,
145146
} from './ReactFiberConfig';
146147
import {
147148
captureCommitPhaseError,
@@ -154,6 +155,7 @@ import {
154155
addMarkerProgressCallbackToPendingTransition,
155156
addMarkerIncompleteCallbackToPendingTransition,
156157
addMarkerCompleteCallbackToPendingTransition,
158+
retryDehydratedSuspenseBoundary,
157159
} from './ReactFiberWorkLoop';
158160
import {
159161
HasEffect as HookHasEffect,
@@ -526,6 +528,23 @@ function commitLayoutEffectOnFiber(
526528
if (flags & Update) {
527529
commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
528530
}
531+
if (flags & Callback) {
532+
// This Boundary is in fallback and has a dehydrated Suspense instance.
533+
// We could in theory assume the dehydrated state but we recheck it for
534+
// certainty.
535+
const finishedState: SuspenseState | null = finishedWork.memoizedState;
536+
if (finishedState !== null) {
537+
const dehydrated = finishedState.dehydrated;
538+
if (dehydrated !== null) {
539+
// Register a callback to retry this boundary once the server has sent the result.
540+
const retry = retryDehydratedSuspenseBoundary.bind(
541+
null,
542+
finishedWork,
543+
);
544+
registerSuspenseInstanceRetry(dehydrated, retry);
545+
}
546+
}
547+
}
529548
break;
530549
}
531550
case OffscreenComponent: {

0 commit comments

Comments
 (0)