9
10
import type {Fiber} from './ReactInternalTypes';
11
import type {StackCursor} from './ReactFiberStack';
12
-import type {SuspenseState, SuspenseProps} from './ReactFiberSuspenseComponent';
12
+import type {SuspenseProps, SuspenseState} from './ReactFiberSuspenseComponent';
13
+import type {OffscreenState} from './ReactFiberOffscreenComponent';
14
15
import {enableSuspenseAvoidThisFallback} from 'shared/ReactFeatureFlags';
16
import {createCursor, push, pop} from './ReactFiberStack';
17
import {isCurrentTreeHidden} from './ReactFiberHiddenContext';
17
-import {SuspenseComponent, OffscreenComponent} from './ReactWorkTags';
18
+import {OffscreenComponent} from './ReactWorkTags';
19
20
// The Suspense handler is the boundary that should capture if something
21
// suspends, i.e. it's the nearest `catch` block on the stack.
23
null,
24
);
25
25
-function shouldAvoidedBoundaryCapture(
26
- workInProgress: Fiber,
27
- handlerOnStack: Fiber,
28
- props: any,
29
-): boolean {
30
- if (enableSuspenseAvoidThisFallback) {
31
- // If the parent is already showing content, and we're not inside a hidden
32
- // tree, then we should show the avoided fallback.
33
- if (handlerOnStack.alternate !== null && !isCurrentTreeHidden()) {
34
- return true;
35
- }
36
-
37
- // If the handler on the stack is also an avoided boundary, then we should
38
- // favor this inner one.
39
- if (
40
- handlerOnStack.tag === SuspenseComponent &&
41
- handlerOnStack.memoizedProps.unstable_avoidThisFallback === true
42
- ) {
43
- return true;
44
- }
45
-
46
- // If this avoided boundary is dehydrated, then it should capture.
47
- const suspenseState: SuspenseState | null = workInProgress.memoizedState;
48
- if (suspenseState !== null && suspenseState.dehydrated !== null) {
49
- return true;
50
- }
51
- }
52
-
53
- // If none of those cases apply, then we should avoid this fallback and show
54
- // the outer one instead.
55
- return false;
56
-}
57
-
58
-export function isBadSuspenseFallback(
59
- current: Fiber | null,
60
- nextProps: SuspenseProps,
61
-): boolean {
62
- // Check if this is a "bad" fallback state or a good one. A bad fallback state
63
- // is one that we only show as a last resort; if this is a transition, we'll
64
- // block it from displaying, and wait for more data to arrive.
65
- if (current !== null) {
66
- const prevState: SuspenseState = current.memoizedState;
67
- const isShowingFallback = prevState !== null;
68
- if (!isShowingFallback && !isCurrentTreeHidden()) {
69
- // It's bad to switch to a fallback if content is already visible
70
- return true;
71
- }
72
- }
73
-
74
- if (
75
- enableSuspenseAvoidThisFallback &&
76
- nextProps.unstable_avoidThisFallback === true
77
- ) {
78
- // Experimental: Some fallbacks are always bad
79
- return true;
80
- }
81
-
82
- return false;
26
+// Represents the outermost boundary that is not visible in the current tree.
27
+// Everything above this is the "shell". When this is null, it means we're
28
+// rendering in the shell of the app. If it's non-null, it means we're rendering
29
+// deeper than the shell, inside a new tree that wasn't already visible.
30
+//
31
+// The main way we use this concept is to determine whether showing a fallback
32
+// would result in a desirable or undesirable loading state. Activing a fallback
33
+// in the shell is considered an undersirable loading state, because it would
34
+// mean hiding visible (albeit stale) content in the current tree — we prefer to
35
+// show the stale content, rather than switch to a fallback. But showing a
36
+// fallback in a new tree is fine, because there's no stale content to
37
+// prefer instead.
38
+let shellBoundary: Fiber | null = null;
39
+
40
+export function getShellBoundary(): Fiber | null {
41
+ return shellBoundary;
42
}
43
44
export function pushPrimaryTreeSuspenseHandler(handler: Fiber): void {
86
- const props = handler.pendingProps;
87
- const handlerOnStack = suspenseHandlerStackCursor.current;
45
+ // TODO: Pass as argument
46
+ const current = handler.alternate;
47
+ const props: SuspenseProps = handler.pendingProps;
48
+
49
+ // Experimental feature: Some Suspense boundaries are marked as having an
50
+ // undesirable fallback state. These have special behavior where we only
51
+ // activate the fallback if there's no other boundary on the stack that we can
52
+ // use instead.
53
if (
54
enableSuspenseAvoidThisFallback &&
55
props.unstable_avoidThisFallback === true &&
91
- handlerOnStack !== null &&
92
- !shouldAvoidedBoundaryCapture(handler, handlerOnStack, props)
56
+ // If an avoided boundary is already visible, it behaves identically to
57
+ // a regular Suspense boundary.
58
+ (current === null || isCurrentTreeHidden())
59
) {
94
- // This boundary should not capture if something suspends. Reuse the
95
- // existing handler on the stack.
96
- push(suspenseHandlerStackCursor, handlerOnStack, handler);
97
- } else {
98
- // Push this handler onto the stack.
99
- push(suspenseHandlerStackCursor, handler, handler);
60
+ if (shellBoundary === null) {
61
+ // We're rendering in the shell. There's no parent Suspense boundary that
62
+ // can provide a desirable fallback state. We'll use this boundary.
63
+ push(suspenseHandlerStackCursor, handler, handler);
64
+
65
+ // However, because this is not a desirable fallback, the children are
66
+ // still considered part of the shell. So we intentionally don't assign
67
+ // to `shellBoundary`.
68
+ } else {
69
+ // There's already a parent Suspense boundary that can provide a desirable
70
+ // fallback state. Prefer that one.
71
+ const handlerOnStack = suspenseHandlerStackCursor.current;
72
+ push(suspenseHandlerStackCursor, handlerOnStack, handler);
73
+ }
74
+ return;
75
+ }
76
+
77
+ // TODO: If the parent Suspense handler already suspended, there's no reason
78
+ // to push a nested Suspense handler, because it will get replaced by the
79
+ // outer fallback, anyway. Consider this as a future optimization.
80
+ push(suspenseHandlerStackCursor, handler, handler);
81
+ if (shellBoundary === null) {
82
+ if (current === null || isCurrentTreeHidden()) {
83
+ // This boundary is not visible in the current UI.
84
+ shellBoundary = handler;
85
+ } else {
86
+ const prevState: SuspenseState = current.memoizedState;
87
+ if (prevState !== null) {
88
+ // This boundary is showing a fallback in the current UI.
89
+ shellBoundary = handler;
90
+ }
91
+ }
92
}
93
}
94
102
export function pushOffscreenSuspenseHandler(fiber: Fiber): void {
103
if (fiber.tag === OffscreenComponent) {
104
push(suspenseHandlerStackCursor, fiber, fiber);
105
+ if (shellBoundary !== null) {
106
+ // A parent boundary is showing a fallback, so we've already rendered
107
+ // deeper than the shell.
108
+ } else {
109
+ const current = fiber.alternate;
110
+ if (current !== null) {
111
+ const prevState: OffscreenState = current.memoizedState;
112
+ if (prevState !== null) {
113
+ // This is the first boundary in the stack that's already showing
114
+ // a fallback. So everything outside is considered the shell.
115
+ shellBoundary = fiber;
116
+ }
117
+ }
118
+ }
119
} else {
120
// This is a LegacyHidden component.
121
reuseSuspenseHandlerOnStack(fiber);
132
133
export function popSuspenseHandler(fiber: Fiber): void {
134
pop(suspenseHandlerStackCursor, fiber);
135
+ if (shellBoundary === fiber) {
136
+ // Popping back into the shell.
137
+ shellBoundary = null;
138
+ }
139
}
140
141
// SuspenseList context