Skip to content

Commit 1859329

Browse files
authored
Track nearest Suspense handler on stack (#24585)
* [FORKED] Add HiddenContext to track if subtree is hidden This adds a new stack cursor for tracking whether we're rendering inside a subtree that's currently hidden. This corresponds to the same place where we're already tracking the "base lanes" needed to reveal a hidden subtree — that is, when going from hidden -> visible, the base lanes are the ones that we skipped over when we deferred the subtree. We must includes all the base lanes and their updates in order to avoid an inconsistency with the surrounding content that already committed. I consolidated the base lanes logic and the hidden logic into the same set of push/pop calls. This is intended to replace the InvisibleParentContext that is currently part of SuspenseContext, but I haven't done that part yet. * Add previous commit to forked revisions * [FORKED] Track nearest Suspense handler on stack Instead of traversing the return path whenever something suspends to find the nearest Suspense boundary, we can push the Suspense boundary onto the stack before entering its subtree. This doesn't affect the overall algorithm that much, but because we already do all the same logic in the begin phase, we can save some redundant work by tracking that information on the stack instead of recomputing it every time. * Add previous commit to forked revisions
1 parent a7b192e commit 1859329

10 files changed

+316
-265
lines changed

Diff for: packages/react-reconciler/src/ReactFiberBeginWork.new.js

+86-94
Large diffs are not rendered by default.

Diff for: packages/react-reconciler/src/ReactFiberCompleteWork.new.js

+38-38
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import type {
2727
SuspenseState,
2828
SuspenseListRenderState,
2929
} from './ReactFiberSuspenseComponent.new';
30-
import type {SuspenseContext} from './ReactFiberSuspenseContext.new';
3130
import type {OffscreenState} from './ReactFiberOffscreenComponent';
3231
import type {Cache} from './ReactFiberCacheComponent.new';
3332
import {
@@ -109,14 +108,17 @@ import {
109108
} from './ReactFiberHostContext.new';
110109
import {
111110
suspenseStackCursor,
112-
InvisibleParentSuspenseContext,
113-
hasSuspenseContext,
114-
popSuspenseContext,
115-
pushSuspenseContext,
116-
setShallowSuspenseContext,
111+
popSuspenseListContext,
112+
popSuspenseHandler,
113+
pushSuspenseListContext,
114+
setShallowSuspenseListContext,
117115
ForceSuspenseFallback,
118-
setDefaultShallowSuspenseContext,
116+
setDefaultShallowSuspenseListContext,
119117
} from './ReactFiberSuspenseContext.new';
118+
import {
119+
popHiddenContext,
120+
isCurrentTreeHidden,
121+
} from './ReactFiberHiddenContext.new';
120122
import {findFirstSuspended} from './ReactFiberSuspenseComponent.new';
121123
import {
122124
isContextProvider as isLegacyContextProvider,
@@ -146,9 +148,7 @@ import {
146148
renderDidSuspend,
147149
renderDidSuspendDelayIfPossible,
148150
renderHasNotSuspendedYet,
149-
popRenderLanes,
150151
getRenderTargetTime,
151-
subtreeRenderLanes,
152152
getWorkInProgressTransitions,
153153
} from './ReactFiberWorkLoop.new';
154154
import {
@@ -1077,7 +1077,7 @@ function completeWork(
10771077
return null;
10781078
}
10791079
case SuspenseComponent: {
1080-
popSuspenseContext(workInProgress);
1080+
popSuspenseHandler(workInProgress);
10811081
const nextState: null | SuspenseState = workInProgress.memoizedState;
10821082

10831083
// Special path for dehydrated boundaries. We may eventually move this
@@ -1186,25 +1186,23 @@ function completeWork(
11861186
// If this render already had a ping or lower pri updates,
11871187
// and this is the first time we know we're going to suspend we
11881188
// should be able to immediately restart from within throwException.
1189-
const hasInvisibleChildContext =
1190-
current === null &&
1191-
(workInProgress.memoizedProps.unstable_avoidThisFallback !==
1192-
true ||
1193-
!enableSuspenseAvoidThisFallback);
1194-
if (
1195-
hasInvisibleChildContext ||
1196-
hasSuspenseContext(
1197-
suspenseStackCursor.current,
1198-
(InvisibleParentSuspenseContext: SuspenseContext),
1199-
)
1200-
) {
1201-
// If this was in an invisible tree or a new render, then showing
1202-
// this boundary is ok.
1203-
renderDidSuspend();
1204-
} else {
1205-
// Otherwise, we're going to have to hide content so we should
1206-
// suspend for longer if possible.
1189+
1190+
// Check if this is a "bad" fallback state or a good one. A bad
1191+
// fallback state is one that we only show as a last resort; if this
1192+
// is a transition, we'll block it from displaying, and wait for
1193+
// more data to arrive.
1194+
const isBadFallback =
1195+
// It's bad to switch to a fallback if content is already visible
1196+
(current !== null && !prevDidTimeout && !isCurrentTreeHidden()) ||
1197+
// Experimental: Some fallbacks are always bad
1198+
(enableSuspenseAvoidThisFallback &&
1199+
workInProgress.memoizedProps.unstable_avoidThisFallback ===
1200+
true);
1201+
1202+
if (isBadFallback) {
12071203
renderDidSuspendDelayIfPossible();
1204+
} else {
1205+
renderDidSuspend();
12081206
}
12091207
}
12101208
}
@@ -1266,7 +1264,7 @@ function completeWork(
12661264
return null;
12671265
}
12681266
case SuspenseListComponent: {
1269-
popSuspenseContext(workInProgress);
1267+
popSuspenseListContext(workInProgress);
12701268

12711269
const renderState: null | SuspenseListRenderState =
12721270
workInProgress.memoizedState;
@@ -1332,11 +1330,11 @@ function completeWork(
13321330
workInProgress.subtreeFlags = NoFlags;
13331331
resetChildFibers(workInProgress, renderLanes);
13341332

1335-
// Set up the Suspense Context to force suspense and immediately
1336-
// rerender the children.
1337-
pushSuspenseContext(
1333+
// Set up the Suspense List Context to force suspense and
1334+
// immediately rerender the children.
1335+
pushSuspenseListContext(
13381336
workInProgress,
1339-
setShallowSuspenseContext(
1337+
setShallowSuspenseListContext(
13401338
suspenseStackCursor.current,
13411339
ForceSuspenseFallback,
13421340
),
@@ -1459,14 +1457,16 @@ function completeWork(
14591457
// setting it the first time we go from not suspended to suspended.
14601458
let suspenseContext = suspenseStackCursor.current;
14611459
if (didSuspendAlready) {
1462-
suspenseContext = setShallowSuspenseContext(
1460+
suspenseContext = setShallowSuspenseListContext(
14631461
suspenseContext,
14641462
ForceSuspenseFallback,
14651463
);
14661464
} else {
1467-
suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
1465+
suspenseContext = setDefaultShallowSuspenseListContext(
1466+
suspenseContext,
1467+
);
14681468
}
1469-
pushSuspenseContext(workInProgress, suspenseContext);
1469+
pushSuspenseListContext(workInProgress, suspenseContext);
14701470
// Do a pass over the next row.
14711471
// Don't bubble properties in this case.
14721472
return next;
@@ -1499,7 +1499,7 @@ function completeWork(
14991499
}
15001500
case OffscreenComponent:
15011501
case LegacyHiddenComponent: {
1502-
popRenderLanes(workInProgress);
1502+
popHiddenContext(workInProgress);
15031503
const nextState: OffscreenState | null = workInProgress.memoizedState;
15041504
const nextIsHidden = nextState !== null;
15051505

@@ -1520,7 +1520,7 @@ function completeWork(
15201520
} else {
15211521
// Don't bubble properties for hidden children unless we're rendering
15221522
// at offscreen priority.
1523-
if (includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane))) {
1523+
if (includesSomeLane(renderLanes, (OffscreenLane: Lane))) {
15241524
bubbleProperties(workInProgress);
15251525
// Check if there was an insertion or update in the hidden subtree.
15261526
// If so, we need to hide those nodes in the commit phase, so
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {Fiber} from './ReactInternalTypes';
11+
import type {StackCursor} from './ReactFiberStack.new';
12+
import type {Lanes} from './ReactFiberLane.new';
13+
14+
import {createCursor, push, pop} from './ReactFiberStack.new';
15+
16+
import {getRenderLanes, setRenderLanes} from './ReactFiberWorkLoop.new';
17+
import {NoLanes, mergeLanes} from './ReactFiberLane.new';
18+
19+
// TODO: Remove `renderLanes` context in favor of hidden context
20+
type HiddenContext = {
21+
// Represents the lanes that must be included when processing updates in
22+
// order to reveal the hidden content.
23+
// TODO: Remove `subtreeLanes` context from work loop in favor of this one.
24+
baseLanes: number,
25+
};
26+
27+
// TODO: This isn't being used yet, but it's intended to replace the
28+
// InvisibleParentContext that is currently managed by SuspenseContext.
29+
export const currentTreeHiddenStackCursor: StackCursor<HiddenContext | null> = createCursor(
30+
null,
31+
);
32+
export const prevRenderLanesStackCursor: StackCursor<Lanes> = createCursor(
33+
NoLanes,
34+
);
35+
36+
export function pushHiddenContext(fiber: Fiber, context: HiddenContext): void {
37+
const prevRenderLanes = getRenderLanes();
38+
push(prevRenderLanesStackCursor, prevRenderLanes, fiber);
39+
push(currentTreeHiddenStackCursor, context, fiber);
40+
41+
// When rendering a subtree that's currently hidden, we must include all
42+
// lanes that would have rendered if the hidden subtree hadn't been deferred.
43+
// That is, in order to reveal content from hidden -> visible, we must commit
44+
// all the updates that we skipped when we originally hid the tree.
45+
setRenderLanes(mergeLanes(prevRenderLanes, context.baseLanes));
46+
}
47+
48+
export function reuseHiddenContextOnStack(fiber: Fiber): void {
49+
// This subtree is not currently hidden, so we don't need to add any lanes
50+
// to the render lanes. But we still need to push something to avoid a
51+
// context mismatch. Reuse the existing context on the stack.
52+
push(prevRenderLanesStackCursor, getRenderLanes(), fiber);
53+
push(
54+
currentTreeHiddenStackCursor,
55+
currentTreeHiddenStackCursor.current,
56+
fiber,
57+
);
58+
}
59+
60+
export function popHiddenContext(fiber: Fiber): void {
61+
// Restore the previous render lanes from the stack
62+
setRenderLanes(prevRenderLanesStackCursor.current);
63+
64+
pop(currentTreeHiddenStackCursor, fiber);
65+
pop(prevRenderLanesStackCursor, fiber);
66+
}
67+
68+
export function isCurrentTreeHidden() {
69+
return currentTreeHiddenStackCursor.current !== null;
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// Intentionally blank. File only exists in new reconciler fork.

Diff for: packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js

-32
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import type {SuspenseInstance} from './ReactFiberHostConfig';
1313
import type {Lane} from './ReactFiberLane.new';
1414
import type {TreeContext} from './ReactFiberTreeContext.new';
1515

16-
import {enableSuspenseAvoidThisFallback} from 'shared/ReactFeatureFlags';
1716
import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags';
1817
import {NoFlags, DidCapture} from './ReactFiberFlags';
1918
import {
@@ -67,37 +66,6 @@ export type SuspenseListRenderState = {|
6766
tailMode: SuspenseListTailMode,
6867
|};
6968

70-
export function shouldCaptureSuspense(
71-
workInProgress: Fiber,
72-
hasInvisibleParent: boolean,
73-
): boolean {
74-
// If it was the primary children that just suspended, capture and render the
75-
// fallback. Otherwise, don't capture and bubble to the next boundary.
76-
const nextState: SuspenseState | null = workInProgress.memoizedState;
77-
if (nextState !== null) {
78-
if (nextState.dehydrated !== null) {
79-
// A dehydrated boundary always captures.
80-
return true;
81-
}
82-
return false;
83-
}
84-
const props = workInProgress.memoizedProps;
85-
// Regular boundaries always capture.
86-
if (
87-
!enableSuspenseAvoidThisFallback ||
88-
props.unstable_avoidThisFallback !== true
89-
) {
90-
return true;
91-
}
92-
// If it's a boundary we should avoid, then we prefer to bubble up to the
93-
// parent boundary if it is currently invisible.
94-
if (hasInvisibleParent) {
95-
return false;
96-
}
97-
// If the parent is not able to handle it, we must handle it.
98-
return true;
99-
}
100-
10169
export function findFirstSuspended(row: Fiber): null | Fiber {
10270
let node = row;
10371
while (node !== null) {

0 commit comments

Comments
 (0)