@samitouri / QOS-React-2 / commits / 961b625ab5

Try not. Do... or do not. Hydrate Suspense Boundaries. (#32851)

Assertively claim a SuspenseInstance. We already know we're hydrating. If there's no match, it throws anyway. So there's no other code path.

Sebastian Markbåge committed Apr 11, 2025 at 10:52 UTC 961b625ab5d180180e836e0c7b221789f0ee336b
2 files changed +22 -30
packages/react-reconciler/src/ReactFiberBeginWork.js
+9 -20
@@ -203,7 +203,6 @@ import {
203 pushFallbackTreeSuspenseHandler,
204 pushOffscreenSuspenseHandler,
205 reuseSuspenseHandlerOnStack,
206 - popSuspenseHandler,
206 } from './ReactFiberSuspenseContext';
207 import {
208 pushHiddenContext,
@@ -245,7 +244,7 @@ import {
244 claimHydratableSingleton,
245 tryToClaimNextHydratableInstance,
246 tryToClaimNextHydratableTextInstance,
248 - tryToClaimNextHydratableSuspenseInstance,
247 + claimNextHydratableSuspenseInstance,
248 warnIfHydrating,
249 queueHydrationError,
250 } from './ReactFiberHydrationContext';
@@ -2151,24 +2150,14 @@ function updateSuspenseComponent(
2150 } else {
2151 pushFallbackTreeSuspenseHandler(workInProgress);
2152 }
2154 - tryToClaimNextHydratableSuspenseInstance(workInProgress);
2155 - // This could've been a dehydrated suspense component.
2156 - const suspenseState: null | SuspenseState = workInProgress.memoizedState;
2157 - if (suspenseState !== null) {
2158 - const dehydrated = suspenseState.dehydrated;
2159 - if (dehydrated !== null) {
2160 - return mountDehydratedSuspenseComponent(
2161 - workInProgress,
2162 - dehydrated,
2163 - renderLanes,
2164 - );
2165 - }
2166 - }
2167 - // If hydration didn't succeed, fall through to the normal Suspense path.
2168 - // To avoid a stack mismatch we need to pop the Suspense handler that we
2169 - // pushed above. This will become less awkward when move the hydration
2170 - // logic to its own fiber.
2171 - popSuspenseHandler(workInProgress);
2153 + // This throws if we fail to hydrate.
2154 + const dehydrated: SuspenseInstance =
2155 + claimNextHydratableSuspenseInstance(workInProgress);
2156 + return mountDehydratedSuspenseComponent(
2157 + workInProgress,
2158 + dehydrated,
2159 + renderLanes,
2160 + );
2161 }
2162
2163 const nextPrimaryChildren = nextProps.children;
packages/react-reconciler/src/ReactFiberHydrationContext.js
+13 -10
@@ -272,7 +272,10 @@ function tryHydrateText(fiber: Fiber, nextInstance: any) {
272 return false;
273 }
274
275 -function tryHydrateSuspense(fiber: Fiber, nextInstance: any) {
275 +function tryHydrateSuspense(
276 + fiber: Fiber,
277 + nextInstance: any,
278 +): null | SuspenseInstance {
279 // fiber is a SuspenseComponent Fiber
280 const suspenseInstance = canHydrateSuspenseInstance(
281 nextInstance,
@@ -298,9 +301,8 @@ function tryHydrateSuspense(fiber: Fiber, nextInstance: any) {
301 // While a Suspense Instance does have children, we won't step into
302 // it during the first pass. Instead, we'll reenter it later.
303 nextHydratableInstance = null;
301 - return true;
304 }
303 - return false;
305 + return suspenseInstance;
306 }
307
308 export const HydrationMismatchException: mixed = new Error(
@@ -423,15 +425,16 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
425 }
426 }
427
426 -function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void {
427 - if (!isHydrating) {
428 - return;
429 - }
428 +function claimNextHydratableSuspenseInstance(fiber: Fiber): SuspenseInstance {
429 const nextInstance = nextHydratableInstance;
431 - if (!nextInstance || !tryHydrateSuspense(fiber, nextInstance)) {
430 + const suspenseInstance = nextInstance
431 + ? tryHydrateSuspense(fiber, nextInstance)
432 + : null;
433 + if (suspenseInstance === null) {
434 warnNonHydratedInstance(fiber, nextInstance);
433 - throwOnHydrationMismatch(fiber);
435 + throw throwOnHydrationMismatch(fiber);
436 }
437 + return suspenseInstance;
438 }
439
440 export function tryToClaimNextHydratableFormMarkerInstance(
@@ -790,7 +793,7 @@ export {
793 claimHydratableSingleton,
794 tryToClaimNextHydratableInstance,
795 tryToClaimNextHydratableTextInstance,
793 - tryToClaimNextHydratableSuspenseInstance,
796 + claimNextHydratableSuspenseInstance,
797 prepareToHydrateHostInstance,
798 prepareToHydrateHostTextInstance,
799 prepareToHydrateHostSuspenseInstance,