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