| 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 | } |