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 {Fiber} from './ReactInternalTypes';
    
  11. import type {StackCursor} from './ReactFiberStack';
    
  12. import type {SuspenseProps, SuspenseState} from './ReactFiberSuspenseComponent';
    
  13. import type {OffscreenState} from './ReactFiberActivityComponent';
    
  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.   pushSuspenseListContext(
    
  52.     handler,
    
  53.     setDefaultShallowSuspenseListContext(suspenseStackCursor.current),
    
  54.   );
    
  55. 
    
  56.   // Experimental feature: Some Suspense boundaries are marked as having an
    
  57.   // undesirable fallback state. These have special behavior where we only
    
  58.   // activate the fallback if there's no other boundary on the stack that we can
    
  59.   // use instead.
    
  60.   if (
    
  61.     enableSuspenseAvoidThisFallback &&
    
  62.     props.unstable_avoidThisFallback === true &&
    
  63.     // If an avoided boundary is already visible, it behaves identically to
    
  64.     // a regular Suspense boundary.
    
  65.     (current === null || isCurrentTreeHidden())
    
  66.   ) {
    
  67.     if (shellBoundary === null) {
    
  68.       // We're rendering in the shell. There's no parent Suspense boundary that
    
  69.       // can provide a desirable fallback state. We'll use this boundary.
    
  70.       push(suspenseHandlerStackCursor, handler, handler);
    
  71. 
    
  72.       // However, because this is not a desirable fallback, the children are
    
  73.       // still considered part of the shell. So we intentionally don't assign
    
  74.       // to `shellBoundary`.
    
  75.     } else {
    
  76.       // There's already a parent Suspense boundary that can provide a desirable
    
  77.       // fallback state. Prefer that one.
    
  78.       const handlerOnStack = suspenseHandlerStackCursor.current;
    
  79.       push(suspenseHandlerStackCursor, handlerOnStack, handler);
    
  80.     }
    
  81.     return;
    
  82.   }
    
  83. 
    
  84.   // TODO: If the parent Suspense handler already suspended, there's no reason
    
  85.   // to push a nested Suspense handler, because it will get replaced by the
    
  86.   // outer fallback, anyway. Consider this as a future optimization.
    
  87.   push(suspenseHandlerStackCursor, handler, handler);
    
  88.   if (shellBoundary === null) {
    
  89.     if (current === null || isCurrentTreeHidden()) {
    
  90.       // This boundary is not visible in the current UI.
    
  91.       shellBoundary = handler;
    
  92.     } else {
    
  93.       const prevState: SuspenseState = current.memoizedState;
    
  94.       if (prevState !== null) {
    
  95.         // This boundary is showing a fallback in the current UI.
    
  96.         shellBoundary = handler;
    
  97.       }
    
  98.     }
    
  99.   }
    
  100. }
    
  101. 
    
  102. export function pushFallbackTreeSuspenseHandler(fiber: Fiber): void {
    
  103.   // We're about to render the fallback. If something in the fallback suspends,
    
  104.   // it's akin to throwing inside of a `catch` block. This boundary should not
    
  105.   // capture. Reuse the existing handler on the stack.
    
  106.   reuseSuspenseHandlerOnStack(fiber);
    
  107. }
    
  108. 
    
  109. export function pushOffscreenSuspenseHandler(fiber: Fiber): void {
    
  110.   if (fiber.tag === OffscreenComponent) {
    
  111.     // A SuspenseList context is only pushed here to avoid a push/pop mismatch.
    
  112.     // Reuse the current value on the stack.
    
  113.     // TODO: We can avoid needing to push here by by forking popSuspenseHandler
    
  114.     // into separate functions for Suspense and Offscreen.
    
  115.     pushSuspenseListContext(fiber, suspenseStackCursor.current);
    
  116.     push(suspenseHandlerStackCursor, fiber, fiber);
    
  117.     if (shellBoundary !== null) {
    
  118.       // A parent boundary is showing a fallback, so we've already rendered
    
  119.       // deeper than the shell.
    
  120.     } else {
    
  121.       const current = fiber.alternate;
    
  122.       if (current !== null) {
    
  123.         const prevState: OffscreenState = current.memoizedState;
    
  124.         if (prevState !== null) {
    
  125.           // This is the first boundary in the stack that's already showing
    
  126.           // a fallback. So everything outside is considered the shell.
    
  127.           shellBoundary = fiber;
    
  128.         }
    
  129.       }
    
  130.     }
    
  131.   } else {
    
  132.     // This is a LegacyHidden component.
    
  133.     reuseSuspenseHandlerOnStack(fiber);
    
  134.   }
    
  135. }
    
  136. 
    
  137. export function reuseSuspenseHandlerOnStack(fiber: Fiber) {
    
  138.   pushSuspenseListContext(fiber, suspenseStackCursor.current);
    
  139.   push(suspenseHandlerStackCursor, getSuspenseHandler(), fiber);
    
  140. }
    
  141. 
    
  142. export function getSuspenseHandler(): Fiber | null {
    
  143.   return suspenseHandlerStackCursor.current;
    
  144. }
    
  145. 
    
  146. export function popSuspenseHandler(fiber: Fiber): void {
    
  147.   pop(suspenseHandlerStackCursor, fiber);
    
  148.   if (shellBoundary === fiber) {
    
  149.     // Popping back into the shell.
    
  150.     shellBoundary = null;
    
  151.   }
    
  152.   popSuspenseListContext(fiber);
    
  153. }
    
  154. 
    
  155. // SuspenseList context
    
  156. // TODO: Move to a separate module? We may change the SuspenseList
    
  157. // implementation to hide/show in the commit phase, anyway.
    
  158. export opaque type SuspenseContext = number;
    
  159. export opaque type SubtreeSuspenseContext: SuspenseContext = number;
    
  160. export opaque type ShallowSuspenseContext: SuspenseContext = number;
    
  161. 
    
  162. const DefaultSuspenseContext: SuspenseContext = 0b00;
    
  163. 
    
  164. const SubtreeSuspenseContextMask: SuspenseContext = 0b01;
    
  165. 
    
  166. // ForceSuspenseFallback can be used by SuspenseList to force newly added
    
  167. // items into their fallback state during one of the render passes.
    
  168. export const ForceSuspenseFallback: ShallowSuspenseContext = 0b10;
    
  169. 
    
  170. export const suspenseStackCursor: StackCursor<SuspenseContext> = createCursor(
    
  171.   DefaultSuspenseContext,
    
  172. );
    
  173. 
    
  174. export function hasSuspenseListContext(
    
  175.   parentContext: SuspenseContext,
    
  176.   flag: SuspenseContext,
    
  177. ): boolean {
    
  178.   return (parentContext & flag) !== 0;
    
  179. }
    
  180. 
    
  181. export function setDefaultShallowSuspenseListContext(
    
  182.   parentContext: SuspenseContext,
    
  183. ): SuspenseContext {
    
  184.   return parentContext & SubtreeSuspenseContextMask;
    
  185. }
    
  186. 
    
  187. export function setShallowSuspenseListContext(
    
  188.   parentContext: SuspenseContext,
    
  189.   shallowContext: ShallowSuspenseContext,
    
  190. ): SuspenseContext {
    
  191.   return (parentContext & SubtreeSuspenseContextMask) | shallowContext;
    
  192. }
    
  193. 
    
  194. export function pushSuspenseListContext(
    
  195.   fiber: Fiber,
    
  196.   newContext: SuspenseContext,
    
  197. ): void {
    
  198.   push(suspenseStackCursor, newContext, fiber);
    
  199. }
    
  200. 
    
  201. export function popSuspenseListContext(fiber: Fiber): void {
    
  202.   pop(suspenseStackCursor, fiber);
    
  203. }