Skip to content

Commit 0191007

Browse files
committed
Fix: HostHoistable should call suspendResource
Follow up to facebook#26450. In the complete phase of HostHoistable, it should call suspendResource instead of suspenseInstance. I refactored the code a bit to make the branching more clear. There's a test case that covers this but it's currently gated behind a TODO because of another issue, which I'll fix in the next step.
1 parent 8d5047e commit 0191007

File tree

2 files changed

+150
-62
lines changed

2 files changed

+150
-62
lines changed

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 149 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
Props,
1818
Container,
1919
ChildSet,
20+
Resource,
2021
} from './ReactFiberHostConfig';
2122
import type {
2223
SuspenseState,
@@ -112,6 +113,7 @@ import {
112113
maySuspendCommit,
113114
mayResourceSuspendCommit,
114115
preloadInstance,
116+
preloadResource,
115117
} from './ReactFiberHostConfig';
116118
import {
117119
getRootHostContainer,
@@ -511,6 +513,10 @@ function updateHostComponent(
511513
}
512514
}
513515

516+
// This function must be called at the very end of the complete phase, because
517+
// it might throw to suspend, and if the resource immediately loads, the work
518+
// loop will resume rendering as if the work-in-progress completed. So it must
519+
// fully complete.
514520
// TODO: This should ideally move to begin phase, but currently the instance is
515521
// not created until the complete phase. For our existing use cases, host nodes
516522
// that suspend don't have children, so it doesn't matter. But that might not
@@ -521,13 +527,35 @@ function preloadInstanceAndSuspendIfNeeded(
521527
props: Props,
522528
renderLanes: Lanes,
523529
) {
530+
if (!maySuspendCommit(type, props)) {
531+
// If this flag was set previously, we can remove it. The flag
532+
// represents whether this particular set of props might ever need to
533+
// suspend. The safest thing to do is for maySuspendCommit to always
534+
// return true, but if the renderer is reasonably confident that the
535+
// underlying resource won't be evicted, it can return false as a
536+
// performance optimization.
537+
workInProgress.flags &= ~SuspenseyCommit;
538+
return;
539+
}
540+
541+
// Mark this fiber with a flag. This gets set on all host instances
542+
// that might possibly suspend, even if they don't need to suspend
543+
// currently. We use this when revealing a prerendered tree, because
544+
// even though the tree has "mounted", its resources might not have
545+
// loaded yet.
524546
workInProgress.flags |= SuspenseyCommit;
547+
525548
// Check if we're rendering at a "non-urgent" priority. This is the same
526549
// check that `useDeferredValue` does to determine whether it needs to
527550
// defer. This is partly for gradual adoption purposes (i.e. shouldn't start
528551
// suspending until you opt in with startTransition or Suspense) but it
529552
// also happens to be the desired behavior for the concrete use cases we've
530553
// thought of so far, like CSS loading, fonts, images, etc.
554+
//
555+
// We check the "root" render lanes here rather than the "subtree" render
556+
// because during a retry or offscreen prerender, the "subtree" render
557+
// lanes may include additional "base" lanes that were deferred during
558+
// a previous render.
531559
// TODO: We may decide to expose a way to force a fallback even during a
532560
// sync update.
533561
if (!includesOnlyNonUrgentLanes(renderLanes)) {
@@ -553,6 +581,35 @@ function preloadInstanceAndSuspendIfNeeded(
553581
}
554582
}
555583

584+
function preloadResourceAndSuspendIfNeeded(
585+
workInProgress: Fiber,
586+
resource: Resource,
587+
type: Type,
588+
props: Props,
589+
renderLanes: Lanes,
590+
) {
591+
// This is a fork of preloadInstanceAndSuspendIfNeeded, but for resources.
592+
if (!mayResourceSuspendCommit(resource)) {
593+
workInProgress.flags &= ~SuspenseyCommit;
594+
return;
595+
}
596+
597+
workInProgress.flags |= SuspenseyCommit;
598+
599+
if (!includesOnlyNonUrgentLanes(renderLanes)) {
600+
// This is an urgent render. Don't suspend or show a fallback.
601+
} else {
602+
const isReady = preloadResource(resource);
603+
if (!isReady) {
604+
if (shouldRemainOnPreviousScreen()) {
605+
// It's OK to suspend. Continue rendering.
606+
} else {
607+
suspendCommit();
608+
}
609+
}
610+
}
611+
}
612+
556613
function scheduleRetryEffect(
557614
workInProgress: Fiber,
558615
retryQueue: RetryQueue | null,
@@ -1015,64 +1072,99 @@ function completeWork(
10151072
}
10161073
case HostHoistable: {
10171074
if (enableFloat && supportsResources) {
1018-
const currentRef = current ? current.ref : null;
1019-
if (currentRef !== workInProgress.ref) {
1020-
markRef(workInProgress);
1021-
}
1022-
1023-
let maySuspend = false;
1075+
// The branching here is more complicated than you might expect because
1076+
// a HostHoistable sometimes corresponds to a Resource and sometimes
1077+
// corresponds to an Instance. It can also switch during an update.
10241078

1025-
// @TODO refactor this block to create the instance here in complete phase if we
1026-
// are not hydrating.
1027-
if (
1079+
const type = workInProgress.type;
1080+
const nextResource: Resource | null = workInProgress.memoizedState;
1081+
if (current === null) {
10281082
// We are mounting and must Update this Hoistable in this commit
1029-
current === null ||
1030-
// We are transitioning to, from, or between Hoistable Resources
1031-
// and require an update
1032-
current.memoizedState !== workInProgress.memoizedState
1033-
) {
1034-
if (workInProgress.memoizedState !== null) {
1035-
maySuspend = mayResourceSuspendCommit(workInProgress.memoizedState);
1083+
// @TODO refactor this block to create the instance here in complete
1084+
// phase if we are not hydrating.
1085+
markUpdate(workInProgress);
1086+
if (workInProgress.ref !== null) {
1087+
markRef(workInProgress);
1088+
}
1089+
if (nextResource !== null) {
1090+
// This is a Hoistable Resource
1091+
1092+
// This must come at the very end of the complete phase.
1093+
bubbleProperties(workInProgress);
1094+
preloadResourceAndSuspendIfNeeded(
1095+
workInProgress,
1096+
nextResource,
1097+
type,
1098+
newProps,
1099+
renderLanes,
1100+
);
1101+
return null;
10361102
} else {
1037-
maySuspend = maySuspendCommit(
1038-
workInProgress.type,
1039-
workInProgress.pendingProps,
1103+
// This is a Hoistable Instance
1104+
1105+
// This must come at the very end of the complete phase.
1106+
bubbleProperties(workInProgress);
1107+
preloadInstanceAndSuspendIfNeeded(
1108+
workInProgress,
1109+
type,
1110+
newProps,
1111+
renderLanes,
10401112
);
1113+
return null;
10411114
}
1042-
markUpdate(workInProgress);
1043-
} else if (workInProgress.memoizedState === null) {
1044-
maySuspend = maySuspendCommit(
1045-
workInProgress.type,
1046-
workInProgress.pendingProps,
1047-
);
1048-
// We may have props to update on the Hoistable instance. We use the
1049-
// updateHostComponent path becuase it produces the update queue
1050-
// we need for Hoistables
1051-
updateHostComponent(
1052-
current,
1053-
workInProgress,
1054-
workInProgress.type,
1055-
workInProgress.pendingProps,
1056-
renderLanes,
1057-
);
1058-
}
1059-
bubbleProperties(workInProgress);
1060-
1061-
// This must come at the very end of the complete phase, because it might
1062-
// throw to suspend, and if the resource immediately loads, the work loop
1063-
// will resume rendering as if the work-in-progress completed. So it must
1064-
// fully complete.
1065-
if (maySuspend) {
1066-
preloadInstanceAndSuspendIfNeeded(
1067-
workInProgress,
1068-
workInProgress.type,
1069-
workInProgress.pendingProps,
1070-
renderLanes,
1071-
);
10721115
} else {
1073-
workInProgress.flags &= ~SuspenseyCommit;
1116+
// We are updating.
1117+
const currentResource = current.memoizedState;
1118+
if (nextResource !== currentResource) {
1119+
// We are transitioning to, from, or between Hoistable Resources
1120+
// and require an update
1121+
markUpdate(workInProgress);
1122+
}
1123+
if (current.ref !== workInProgress.ref) {
1124+
markRef(workInProgress);
1125+
}
1126+
if (nextResource !== null) {
1127+
// This is a Hoistable Resource
1128+
// This must come at the very end of the complete phase.
1129+
1130+
bubbleProperties(workInProgress);
1131+
if (nextResource === currentResource) {
1132+
workInProgress.flags &= ~SuspenseyCommit;
1133+
} else {
1134+
preloadResourceAndSuspendIfNeeded(
1135+
workInProgress,
1136+
nextResource,
1137+
type,
1138+
newProps,
1139+
renderLanes,
1140+
);
1141+
}
1142+
return null;
1143+
} else {
1144+
// This is a Hoistable Instance
1145+
//
1146+
// We may have props to update on the Hoistable instance. We use the
1147+
// updateHostComponent path becuase it produces the update queue
1148+
// we need for Hoistables.
1149+
updateHostComponent(
1150+
current,
1151+
workInProgress,
1152+
type,
1153+
newProps,
1154+
renderLanes,
1155+
);
1156+
1157+
// This must come at the very end of the complete phase.
1158+
bubbleProperties(workInProgress);
1159+
preloadInstanceAndSuspendIfNeeded(
1160+
workInProgress,
1161+
type,
1162+
newProps,
1163+
renderLanes,
1164+
);
1165+
return null;
1166+
}
10741167
}
1075-
return null;
10761168
}
10771169
}
10781170
// eslint-disable-next-line-no-fallthrough
@@ -1141,7 +1233,6 @@ function completeWork(
11411233
case HostComponent: {
11421234
popHostContext(workInProgress);
11431235
const type = workInProgress.type;
1144-
const maySuspend = maySuspendCommit(type, newProps);
11451236
if (current !== null && workInProgress.stateNode != null) {
11461237
updateHostComponent(
11471238
current,
@@ -1222,16 +1313,12 @@ function completeWork(
12221313
// throw to suspend, and if the resource immediately loads, the work loop
12231314
// will resume rendering as if the work-in-progress completed. So it must
12241315
// fully complete.
1225-
if (maySuspend) {
1226-
preloadInstanceAndSuspendIfNeeded(
1227-
workInProgress,
1228-
type,
1229-
newProps,
1230-
renderLanes,
1231-
);
1232-
} else {
1233-
workInProgress.flags &= ~SuspenseyCommit;
1234-
}
1316+
preloadInstanceAndSuspendIfNeeded(
1317+
workInProgress,
1318+
workInProgress.type,
1319+
workInProgress.pendingProps,
1320+
renderLanes,
1321+
);
12351322
return null;
12361323
}
12371324
case HostText: {

packages/react-reconciler/src/ReactFiberHostConfigWithNoResources.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ function shim(...args: any): empty {
1919
}
2020

2121
export type HoistableRoot = mixed;
22+
export type Resource = mixed;
2223

2324
// Resources (when unsupported)
2425
export const supportsResources = false;

0 commit comments

Comments
 (0)