Skip to content

Commit 67de5e3

Browse files
committed
[FORKED] Hidden trees should capture Suspense
If something suspends inside a hidden tree, it should not affect anything in the visible part of the UI. This means that Offscreen acts like a Suspense boundary whenever it's in its hidden state.
1 parent c1f5884 commit 67de5e3

14 files changed

+670
-121
lines changed

packages/react-reconciler/src/ReactFiber.new.js

+2
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,7 @@ export function createFiberFromOffscreen(
719719
const primaryChildInstance: OffscreenInstance = {
720720
isHidden: false,
721721
pendingMarkers: null,
722+
retryCache: null,
722723
transitions: null,
723724
};
724725
fiber.stateNode = primaryChildInstance;
@@ -740,6 +741,7 @@ export function createFiberFromLegacyHidden(
740741
isHidden: false,
741742
pendingMarkers: null,
742743
transitions: null,
744+
retryCache: null,
743745
};
744746
fiber.stateNode = instance;
745747
return fiber;

packages/react-reconciler/src/ReactFiber.old.js

+2
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,7 @@ export function createFiberFromOffscreen(
719719
const primaryChildInstance: OffscreenInstance = {
720720
isHidden: false,
721721
pendingMarkers: null,
722+
retryCache: null,
722723
transitions: null,
723724
};
724725
fiber.stateNode = primaryChildInstance;
@@ -740,6 +741,7 @@ export function createFiberFromLegacyHidden(
740741
isHidden: false,
741742
pendingMarkers: null,
742743
transitions: null,
744+
retryCache: null,
743745
};
744746
fiber.stateNode = instance;
745747
return fiber;

packages/react-reconciler/src/ReactFiberBeginWork.new.js

+143-46
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ import {
174174
setShallowSuspenseListContext,
175175
pushPrimaryTreeSuspenseHandler,
176176
pushFallbackTreeSuspenseHandler,
177+
pushOffscreenSuspenseHandler,
178+
reuseSuspenseHandlerOnStack,
177179
popSuspenseHandler,
178180
} from './ReactFiberSuspenseContext.new';
179181
import {
@@ -678,6 +680,52 @@ function updateOffscreenComponent(
678680
(enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding')
679681
) {
680682
// Rendering a hidden tree.
683+
684+
const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;
685+
if (didSuspend) {
686+
// Something suspended inside a hidden tree
687+
688+
// Include the base lanes from the last render
689+
const nextBaseLanes =
690+
prevState !== null
691+
? mergeLanes(prevState.baseLanes, renderLanes)
692+
: renderLanes;
693+
694+
if (current !== null) {
695+
// Reset to the current children
696+
let currentChild = (workInProgress.child = current.child);
697+
698+
// The current render suspended, but there may be other lanes with
699+
// pending work. We can't read `childLanes` from the current Offscreen
700+
// fiber because we reset it when it was deferred; however, we can read
701+
// the pending lanes from the child fibers.
702+
let currentChildLanes = NoLanes;
703+
while (currentChild !== null) {
704+
currentChildLanes = mergeLanes(
705+
mergeLanes(currentChildLanes, currentChild.lanes),
706+
currentChild.childLanes,
707+
);
708+
currentChild = currentChild.sibling;
709+
}
710+
const lanesWeJustAttempted = nextBaseLanes;
711+
const remainingChildLanes = removeLanes(
712+
currentChildLanes,
713+
lanesWeJustAttempted,
714+
);
715+
workInProgress.childLanes = remainingChildLanes;
716+
} else {
717+
workInProgress.childLanes = NoLanes;
718+
workInProgress.child = null;
719+
}
720+
721+
return deferHiddenOffscreenComponent(
722+
current,
723+
workInProgress,
724+
nextBaseLanes,
725+
renderLanes,
726+
);
727+
}
728+
681729
if ((workInProgress.mode & ConcurrentMode) === NoMode) {
682730
// In legacy sync mode, don't defer the subtree. Render it now.
683731
// TODO: Consider how Offscreen should work with transitions in the future
@@ -694,50 +742,28 @@ function updateOffscreenComponent(
694742
}
695743
}
696744
reuseHiddenContextOnStack(workInProgress);
745+
pushOffscreenSuspenseHandler(workInProgress);
697746
} else if (!includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
698747
// We're hidden, and we're not rendering at Offscreen. We will bail out
699748
// and resume this tree later.
700-
let nextBaseLanes = renderLanes;
701-
if (prevState !== null) {
702-
// Include the base lanes from the last render
703-
nextBaseLanes = mergeLanes(nextBaseLanes, prevState.baseLanes);
704-
}
705749

706-
// Schedule this fiber to re-render at offscreen priority. Then bailout.
750+
// Schedule this fiber to re-render at Offscreen priority
707751
workInProgress.lanes = workInProgress.childLanes = laneToLanes(
708752
OffscreenLane,
709753
);
710-
const nextState: OffscreenState = {
711-
baseLanes: nextBaseLanes,
712-
// Save the cache pool so we can resume later.
713-
cachePool: enableCache ? getOffscreenDeferredCache() : null,
714-
};
715-
workInProgress.memoizedState = nextState;
716-
workInProgress.updateQueue = null;
717-
if (enableCache) {
718-
// push the cache pool even though we're going to bail out
719-
// because otherwise there'd be a context mismatch
720-
if (current !== null) {
721-
pushTransition(workInProgress, null, null);
722-
}
723-
}
724-
725-
// We're about to bail out, but we need to push this to the stack anyway
726-
// to avoid a push/pop misalignment.
727-
reuseHiddenContextOnStack(workInProgress);
728754

729-
if (enableLazyContextPropagation && current !== null) {
730-
// Since this tree will resume rendering in a separate render, we need
731-
// to propagate parent contexts now so we don't lose track of which
732-
// ones changed.
733-
propagateParentContextChangesToDeferredTree(
734-
current,
735-
workInProgress,
736-
renderLanes,
737-
);
738-
}
755+
// Include the base lanes from the last render
756+
const nextBaseLanes =
757+
prevState !== null
758+
? mergeLanes(prevState.baseLanes, renderLanes)
759+
: renderLanes;
739760

740-
return null;
761+
return deferHiddenOffscreenComponent(
762+
current,
763+
workInProgress,
764+
nextBaseLanes,
765+
renderLanes,
766+
);
741767
} else {
742768
// This is the second render. The surrounding visible content has already
743769
// committed. Now we resume rendering the hidden tree.
@@ -764,6 +790,7 @@ function updateOffscreenComponent(
764790
} else {
765791
reuseHiddenContextOnStack(workInProgress);
766792
}
793+
pushOffscreenSuspenseHandler(workInProgress);
767794
}
768795
} else {
769796
// Rendering a visible tree.
@@ -791,6 +818,7 @@ function updateOffscreenComponent(
791818

792819
// Push the lanes that were skipped when we bailed out.
793820
pushHiddenContext(workInProgress, prevState);
821+
reuseSuspenseHandlerOnStack(workInProgress);
794822

795823
// Since we're not hidden anymore, reset the state
796824
workInProgress.memoizedState = null;
@@ -811,13 +839,54 @@ function updateOffscreenComponent(
811839
// We're about to bail out, but we need to push this to the stack anyway
812840
// to avoid a push/pop misalignment.
813841
reuseHiddenContextOnStack(workInProgress);
842+
reuseSuspenseHandlerOnStack(workInProgress);
814843
}
815844
}
816845

817846
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
818847
return workInProgress.child;
819848
}
820849

850+
function deferHiddenOffscreenComponent(
851+
current: Fiber | null,
852+
workInProgress: Fiber,
853+
nextBaseLanes: Lanes,
854+
renderLanes: Lanes,
855+
) {
856+
const nextState: OffscreenState = {
857+
baseLanes: nextBaseLanes,
858+
// Save the cache pool so we can resume later.
859+
cachePool: enableCache ? getOffscreenDeferredCache() : null,
860+
};
861+
workInProgress.memoizedState = nextState;
862+
if (enableCache) {
863+
// push the cache pool even though we're going to bail out
864+
// because otherwise there'd be a context mismatch
865+
if (current !== null) {
866+
pushTransition(workInProgress, null, null);
867+
}
868+
}
869+
870+
// We're about to bail out, but we need to push this to the stack anyway
871+
// to avoid a push/pop misalignment.
872+
reuseHiddenContextOnStack(workInProgress);
873+
874+
pushOffscreenSuspenseHandler(workInProgress);
875+
876+
if (enableLazyContextPropagation && current !== null) {
877+
// Since this tree will resume rendering in a separate render, we need
878+
// to propagate parent contexts now so we don't lose track of which
879+
// ones changed.
880+
propagateParentContextChangesToDeferredTree(
881+
current,
882+
workInProgress,
883+
renderLanes,
884+
);
885+
}
886+
887+
return null;
888+
}
889+
821890
// Note: These happen to have identical begin phases, for now. We shouldn't hold
822891
// ourselves to this constraint, though. If the behavior diverges, we should
823892
// fork the function.
@@ -2109,13 +2178,19 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
21092178
if (enableTransitionTracing) {
21102179
const currentTransitions = getPendingTransitions();
21112180
if (currentTransitions !== null) {
2112-
// If there are no transitions, we don't need to keep track of tracing markers
21132181
const parentMarkerInstances = getMarkerInstances();
2114-
const primaryChildUpdateQueue: OffscreenQueue = {
2115-
transitions: currentTransitions,
2116-
markerInstances: parentMarkerInstances,
2117-
};
2118-
primaryChildFragment.updateQueue = primaryChildUpdateQueue;
2182+
const offscreenQueue: OffscreenQueue | null = (primaryChildFragment.updateQueue: any);
2183+
if (offscreenQueue === null) {
2184+
const newOffscreenQueue: OffscreenQueue = {
2185+
transitions: currentTransitions,
2186+
markerInstances: parentMarkerInstances,
2187+
wakeables: null,
2188+
};
2189+
primaryChildFragment.updateQueue = newOffscreenQueue;
2190+
} else {
2191+
offscreenQueue.transitions = currentTransitions;
2192+
offscreenQueue.markerInstances = parentMarkerInstances;
2193+
}
21192194
}
21202195
}
21212196

@@ -2140,6 +2215,8 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
21402215
);
21412216
workInProgress.memoizedState = SUSPENDED_MARKER;
21422217

2218+
// TODO: Transition Tracing is not yet implemented for CPU Suspense.
2219+
21432220
// Since nothing actually suspended, there will nothing to ping this to
21442221
// get it started back up to attempt the next item. While in terms of
21452222
// priority this work has the same priority as this current render, it's
@@ -2201,11 +2278,31 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
22012278
const currentTransitions = getPendingTransitions();
22022279
if (currentTransitions !== null) {
22032280
const parentMarkerInstances = getMarkerInstances();
2204-
const primaryChildUpdateQueue: OffscreenQueue = {
2205-
transitions: currentTransitions,
2206-
markerInstances: parentMarkerInstances,
2207-
};
2208-
primaryChildFragment.updateQueue = primaryChildUpdateQueue;
2281+
const offscreenQueue: OffscreenQueue | null = (primaryChildFragment.updateQueue: any);
2282+
const currentOffscreenQueue: OffscreenQueue | null = (current.updateQueue: any);
2283+
if (offscreenQueue === null) {
2284+
const newOffscreenQueue: OffscreenQueue = {
2285+
transitions: currentTransitions,
2286+
markerInstances: parentMarkerInstances,
2287+
wakeables: null,
2288+
};
2289+
primaryChildFragment.updateQueue = newOffscreenQueue;
2290+
} else if (offscreenQueue === currentOffscreenQueue) {
2291+
// If the work-in-progress queue is the same object as current, we
2292+
// can't modify it without cloning it first.
2293+
const newOffscreenQueue: OffscreenQueue = {
2294+
transitions: currentTransitions,
2295+
markerInstances: parentMarkerInstances,
2296+
wakeables:
2297+
currentOffscreenQueue !== null
2298+
? currentOffscreenQueue.wakeables
2299+
: null,
2300+
};
2301+
primaryChildFragment.updateQueue = newOffscreenQueue;
2302+
} else {
2303+
offscreenQueue.transitions = currentTransitions;
2304+
offscreenQueue.markerInstances = parentMarkerInstances;
2305+
}
22092306
}
22102307
}
22112308
primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(

packages/react-reconciler/src/ReactFiberBeginWork.old.js

+2
Original file line numberDiff line numberDiff line change
@@ -2130,6 +2130,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
21302130
const primaryChildUpdateQueue: OffscreenQueue = {
21312131
transitions: currentTransitions,
21322132
markerInstances: parentMarkerInstances,
2133+
wakeables: null,
21332134
};
21342135
primaryChildFragment.updateQueue = primaryChildUpdateQueue;
21352136
}
@@ -2216,6 +2217,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
22162217
const primaryChildUpdateQueue: OffscreenQueue = {
22172218
transitions: currentTransitions,
22182219
markerInstances: parentMarkerInstances,
2220+
wakeables: null,
22192221
};
22202222
primaryChildFragment.updateQueue = primaryChildUpdateQueue;
22212223
}

0 commit comments

Comments
 (0)