Skip to content

Commit 4fe4e06

Browse files
committed
Register Suspense retry handlers in commit phase
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 4fe4e06

File tree

3 files changed

+43
-19
lines changed

3 files changed

+43
-19
lines changed

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

Lines changed: 21 additions & 13 deletions
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

Lines changed: 3 additions & 6 deletions
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

Lines changed: 19 additions & 0 deletions
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)