main
js 234 lines 8.85 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and 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 {SuspenseProps} from 'shared/ReactTypes';
11 import type {Fiber} from './ReactInternalTypes';
12 import type {StackCursor} from './ReactFiberStack';
13 import type {SuspenseState} from './ReactFiberSuspenseComponent';
14
15 import {enableSuspenseAvoidThisFallback} from 'shared/ReactFeatureFlags';
16 import {createCursor, push, pop} from './ReactFiberStack';
17 import {isCurrentTreeHidden} from './ReactFiberHiddenContext';
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.
22 const suspenseHandlerStackCursor: StackCursor<Fiber | null> =
23 createCursor(null);
24
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;
41 }
42
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 // Shallow Suspense context fields, like ForceSuspenseFallback, should only be
49 // propagated a single level. For example, when ForceSuspenseFallback is set,
50 // it should only force the nearest Suspense boundary into fallback mode.
51 push(
52 suspenseStackCursor,
53 setDefaultShallowSuspenseListContext(suspenseStackCursor.current),
54 handler,
55 );
56
57 // Experimental feature: Some Suspense boundaries are marked as having an
58 // undesirable fallback state. These have special behavior where we only
59 // activate the fallback if there's no other boundary on the stack that we can
60 // use instead.
61 if (
62 enableSuspenseAvoidThisFallback &&
63 props.unstable_avoidThisFallback === true &&
64 // If an avoided boundary is already visible, it behaves identically to
65 // a regular Suspense boundary.
66 (current === null || isCurrentTreeHidden())
67 ) {
68 if (shellBoundary === null) {
69 // We're rendering in the shell. There's no parent Suspense boundary that
70 // can provide a desirable fallback state. We'll use this boundary.
71 push(suspenseHandlerStackCursor, handler, handler);
72
73 // However, because this is not a desirable fallback, the children are
74 // still considered part of the shell. So we intentionally don't assign
75 // to `shellBoundary`.
76 } else {
77 // There's already a parent Suspense boundary that can provide a desirable
78 // fallback state. Prefer that one.
79 const handlerOnStack = suspenseHandlerStackCursor.current;
80 push(suspenseHandlerStackCursor, handlerOnStack, handler);
81 }
82 return;
83 }
84
85 // TODO: If the parent Suspense handler already suspended, there's no reason
86 // to push a nested Suspense handler, because it will get replaced by the
87 // outer fallback, anyway. Consider this as a future optimization.
88 push(suspenseHandlerStackCursor, handler, handler);
89 if (shellBoundary === null) {
90 if (current === null || isCurrentTreeHidden()) {
91 // This boundary is not visible in the current UI.
92 shellBoundary = handler;
93 } else {
94 const prevState: SuspenseState = current.memoizedState;
95 // $FlowFixMe[invalid-compare]
96 if (prevState !== null) {
97 // This boundary is showing a fallback in the current UI.
98 shellBoundary = handler;
99 }
100 }
101 }
102 }
103
104 export function pushFallbackTreeSuspenseHandler(fiber: Fiber): void {
105 // We're about to render the fallback. If something in the fallback suspends,
106 // it's akin to throwing inside of a `catch` block. This boundary should not
107 // capture. Reuse the existing handler on the stack.
108 reuseSuspenseHandlerOnStack(fiber);
109 }
110
111 export function pushDehydratedActivitySuspenseHandler(fiber: Fiber): void {
112 // This is called when hydrating an Activity boundary. We can just leave it
113 // dehydrated if it suspends.
114 // A SuspenseList context is only pushed here to avoid a push/pop mismatch.
115 // Reuse the current value on the stack.
116 // TODO: We can avoid needing to push here by by forking popSuspenseHandler
117 // into separate functions for Activity, Suspense and Offscreen.
118 push(suspenseStackCursor, suspenseStackCursor.current, fiber);
119 push(suspenseHandlerStackCursor, fiber, fiber);
120 if (shellBoundary === null) {
121 // We can contain any suspense inside the Activity boundary.
122 shellBoundary = fiber;
123 }
124 }
125
126 export function pushOffscreenSuspenseHandler(fiber: Fiber): void {
127 if (fiber.tag === OffscreenComponent) {
128 // A SuspenseList context is only pushed here to avoid a push/pop mismatch.
129 // Reuse the current value on the stack.
130 // TODO: We can avoid needing to push here by by forking popSuspenseHandler
131 // into separate functions for Activity, Suspense and Offscreen.
132 push(suspenseStackCursor, suspenseStackCursor.current, fiber);
133 push(suspenseHandlerStackCursor, fiber, fiber);
134 if (shellBoundary === null) {
135 // We're rendering hidden content. If it suspends, we can handle it by
136 // just not committing the offscreen boundary.
137 shellBoundary = fiber;
138 }
139 } else {
140 // This is a LegacyHidden component.
141 reuseSuspenseHandlerOnStack(fiber);
142 }
143 }
144
145 export function reuseSuspenseHandlerOnStack(fiber: Fiber) {
146 push(suspenseStackCursor, suspenseStackCursor.current, fiber);
147 push(suspenseHandlerStackCursor, getSuspenseHandler(), fiber);
148 }
149
150 export function getSuspenseHandler(): Fiber | null {
151 return suspenseHandlerStackCursor.current;
152 }
153
154 export function popSuspenseHandler(fiber: Fiber): void {
155 pop(suspenseHandlerStackCursor, fiber);
156 if (shellBoundary === fiber) {
157 // Popping back into the shell.
158 shellBoundary = null;
159 }
160 pop(suspenseStackCursor, fiber);
161 }
162
163 // SuspenseList context
164 // TODO: Move to a separate module? We may change the SuspenseList
165 // implementation to hide/show in the commit phase, anyway.
166 export opaque type SuspenseContext = number;
167 export opaque type SubtreeSuspenseContext: SuspenseContext = number;
168 export opaque type ShallowSuspenseContext: SuspenseContext = number;
169
170 const DefaultSuspenseContext: SuspenseContext = 0b00;
171
172 const SubtreeSuspenseContextMask: SuspenseContext = 0b01;
173
174 // ForceSuspenseFallback can be used by SuspenseList to force newly added
175 // items into their fallback state during one of the render passes.
176 export const ForceSuspenseFallback: ShallowSuspenseContext = 0b10;
177
178 export const suspenseStackCursor: StackCursor<SuspenseContext> = createCursor(
179 DefaultSuspenseContext,
180 );
181
182 export function hasSuspenseListContext(
183 parentContext: SuspenseContext,
184 flag: SuspenseContext,
185 ): boolean {
186 return (parentContext & flag) !== 0;
187 }
188
189 export function setDefaultShallowSuspenseListContext(
190 parentContext: SuspenseContext,
191 ): SuspenseContext {
192 return parentContext & SubtreeSuspenseContextMask;
193 }
194
195 export function setShallowSuspenseListContext(
196 parentContext: SuspenseContext,
197 shallowContext: ShallowSuspenseContext,
198 ): SuspenseContext {
199 return (parentContext & SubtreeSuspenseContextMask) | shallowContext;
200 }
201
202 export function pushSuspenseListContext(
203 fiber: Fiber,
204 newContext: SuspenseContext,
205 ): void {
206 // Push the current handler in this case since we're not catching at the SuspenseList
207 // for typical rows.
208 const handlerOnStack = suspenseHandlerStackCursor.current;
209 push(suspenseHandlerStackCursor, handlerOnStack, fiber);
210 push(suspenseStackCursor, newContext, fiber);
211 }
212
213 export function pushSuspenseListCatch(
214 fiber: Fiber,
215 newContext: SuspenseContext,
216 ): void {
217 // In this case we do want to handle catching suspending on the actual boundary itself.
218 // This is used for rows that are allowed to be hidden anyway.
219 push(suspenseHandlerStackCursor, fiber, fiber);
220 push(suspenseStackCursor, newContext, fiber);
221 if (shellBoundary === null) {
222 // We can contain the effects to hiding the current row.
223 shellBoundary = fiber;
224 }
225 }
226
227 export function popSuspenseListContext(fiber: Fiber): void {
228 pop(suspenseStackCursor, fiber);
229 pop(suspenseHandlerStackCursor, fiber);
230 if (shellBoundary === fiber) {
231 // Popping back into the shell.
232 shellBoundary = null;
233 }
234 }