-
Notifications
You must be signed in to change notification settings - Fork 48.4k
/
Copy pathReactFiberSuspenseContext.js
209 lines (183 loc) · 7.97 KB
/
ReactFiberSuspenseContext.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {SuspenseProps} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {StackCursor} from './ReactFiberStack';
import type {SuspenseState} from './ReactFiberSuspenseComponent';
import {enableSuspenseAvoidThisFallback} from 'shared/ReactFeatureFlags';
import {createCursor, push, pop} from './ReactFiberStack';
import {isCurrentTreeHidden} from './ReactFiberHiddenContext';
import {OffscreenComponent} from './ReactWorkTags';
// The Suspense handler is the boundary that should capture if something
// suspends, i.e. it's the nearest `catch` block on the stack.
const suspenseHandlerStackCursor: StackCursor<Fiber | null> =
createCursor(null);
// Represents the outermost boundary that is not visible in the current tree.
// Everything above this is the "shell". When this is null, it means we're
// rendering in the shell of the app. If it's non-null, it means we're rendering
// deeper than the shell, inside a new tree that wasn't already visible.
//
// The main way we use this concept is to determine whether showing a fallback
// would result in a desirable or undesirable loading state. Activing a fallback
// in the shell is considered an undersirable loading state, because it would
// mean hiding visible (albeit stale) content in the current tree — we prefer to
// show the stale content, rather than switch to a fallback. But showing a
// fallback in a new tree is fine, because there's no stale content to
// prefer instead.
let shellBoundary: Fiber | null = null;
export function getShellBoundary(): Fiber | null {
return shellBoundary;
}
export function pushPrimaryTreeSuspenseHandler(handler: Fiber): void {
// TODO: Pass as argument
const current = handler.alternate;
const props: SuspenseProps = handler.pendingProps;
// Shallow Suspense context fields, like ForceSuspenseFallback, should only be
// propagated a single level. For example, when ForceSuspenseFallback is set,
// it should only force the nearest Suspense boundary into fallback mode.
pushSuspenseListContext(
handler,
setDefaultShallowSuspenseListContext(suspenseStackCursor.current),
);
// Experimental feature: Some Suspense boundaries are marked as having an
// undesirable fallback state. These have special behavior where we only
// activate the fallback if there's no other boundary on the stack that we can
// use instead.
if (
enableSuspenseAvoidThisFallback &&
props.unstable_avoidThisFallback === true &&
// If an avoided boundary is already visible, it behaves identically to
// a regular Suspense boundary.
(current === null || isCurrentTreeHidden())
) {
if (shellBoundary === null) {
// We're rendering in the shell. There's no parent Suspense boundary that
// can provide a desirable fallback state. We'll use this boundary.
push(suspenseHandlerStackCursor, handler, handler);
// However, because this is not a desirable fallback, the children are
// still considered part of the shell. So we intentionally don't assign
// to `shellBoundary`.
} else {
// There's already a parent Suspense boundary that can provide a desirable
// fallback state. Prefer that one.
const handlerOnStack = suspenseHandlerStackCursor.current;
push(suspenseHandlerStackCursor, handlerOnStack, handler);
}
return;
}
// TODO: If the parent Suspense handler already suspended, there's no reason
// to push a nested Suspense handler, because it will get replaced by the
// outer fallback, anyway. Consider this as a future optimization.
push(suspenseHandlerStackCursor, handler, handler);
if (shellBoundary === null) {
if (current === null || isCurrentTreeHidden()) {
// This boundary is not visible in the current UI.
shellBoundary = handler;
} else {
const prevState: SuspenseState = current.memoizedState;
if (prevState !== null) {
// This boundary is showing a fallback in the current UI.
shellBoundary = handler;
}
}
}
}
export function pushFallbackTreeSuspenseHandler(fiber: Fiber): void {
// We're about to render the fallback. If something in the fallback suspends,
// it's akin to throwing inside of a `catch` block. This boundary should not
// capture. Reuse the existing handler on the stack.
reuseSuspenseHandlerOnStack(fiber);
}
export function pushDehydratedActivitySuspenseHandler(fiber: Fiber): void {
// This is called when hydrating an Activity boundary. We can just leave it
// dehydrated if it suspends.
// A SuspenseList context is only pushed here to avoid a push/pop mismatch.
// Reuse the current value on the stack.
// TODO: We can avoid needing to push here by by forking popSuspenseHandler
// into separate functions for Activity, Suspense and Offscreen.
pushSuspenseListContext(fiber, suspenseStackCursor.current);
push(suspenseHandlerStackCursor, fiber, fiber);
if (shellBoundary === null) {
// We can contain any suspense inside the Activity boundary.
shellBoundary = fiber;
}
}
export function pushOffscreenSuspenseHandler(fiber: Fiber): void {
if (fiber.tag === OffscreenComponent) {
// A SuspenseList context is only pushed here to avoid a push/pop mismatch.
// Reuse the current value on the stack.
// TODO: We can avoid needing to push here by by forking popSuspenseHandler
// into separate functions for Activity, Suspense and Offscreen.
pushSuspenseListContext(fiber, suspenseStackCursor.current);
push(suspenseHandlerStackCursor, fiber, fiber);
if (shellBoundary === null) {
// We're rendering hidden content. If it suspends, we can handle it by
// just not committing the offscreen boundary.
shellBoundary = fiber;
}
} else {
// This is a LegacyHidden component.
reuseSuspenseHandlerOnStack(fiber);
}
}
export function reuseSuspenseHandlerOnStack(fiber: Fiber) {
pushSuspenseListContext(fiber, suspenseStackCursor.current);
push(suspenseHandlerStackCursor, getSuspenseHandler(), fiber);
}
export function getSuspenseHandler(): Fiber | null {
return suspenseHandlerStackCursor.current;
}
export function popSuspenseHandler(fiber: Fiber): void {
pop(suspenseHandlerStackCursor, fiber);
if (shellBoundary === fiber) {
// Popping back into the shell.
shellBoundary = null;
}
popSuspenseListContext(fiber);
}
// SuspenseList context
// TODO: Move to a separate module? We may change the SuspenseList
// implementation to hide/show in the commit phase, anyway.
export opaque type SuspenseContext = number;
export opaque type SubtreeSuspenseContext: SuspenseContext = number;
export opaque type ShallowSuspenseContext: SuspenseContext = number;
const DefaultSuspenseContext: SuspenseContext = 0b00;
const SubtreeSuspenseContextMask: SuspenseContext = 0b01;
// ForceSuspenseFallback can be used by SuspenseList to force newly added
// items into their fallback state during one of the render passes.
export const ForceSuspenseFallback: ShallowSuspenseContext = 0b10;
export const suspenseStackCursor: StackCursor<SuspenseContext> = createCursor(
DefaultSuspenseContext,
);
export function hasSuspenseListContext(
parentContext: SuspenseContext,
flag: SuspenseContext,
): boolean {
return (parentContext & flag) !== 0;
}
export function setDefaultShallowSuspenseListContext(
parentContext: SuspenseContext,
): SuspenseContext {
return parentContext & SubtreeSuspenseContextMask;
}
export function setShallowSuspenseListContext(
parentContext: SuspenseContext,
shallowContext: ShallowSuspenseContext,
): SuspenseContext {
return (parentContext & SubtreeSuspenseContextMask) | shallowContext;
}
export function pushSuspenseListContext(
fiber: Fiber,
newContext: SuspenseContext,
): void {
push(suspenseStackCursor, newContext, fiber);
}
export function popSuspenseListContext(fiber: Fiber): void {
pop(suspenseStackCursor, fiber);
}