@samitouri / QOS-React-2 / commits / b2763d3eaa

Move hydration code out of normal Suspense path (#24532)

* Move hydration code out of normal Suspense path Shuffling some code around to make it easier to follow. The logic for updating a dehydrated Suspense boundary is significantly different from the logic for a client-rendered Suspense boundary. Most of it was already lifted out into a separate function; this moves the remaining hydration-specific logic out of updateSuspenseComponent and into updateDehydratedSuspenseComponent instead. No expected changes to program behavior. * Extract hydration logic in complete phase, too Same as previous step but for the complete phase. This is a separate commit to make bisecting easier in case something breaks. The logic is very subtle but mostly all I've done is extract it to another function.

Andrew Clark committed May 25, 2022 at 15:42 UTC b2763d3eaa4a56ce9c973945783aba7cac63478f
4 files changed +682 -698
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+227 -273
@@ -2049,6 +2049,8 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
2049 // a stack.
2050 if (current === null) {
2051 // Initial mount
2052 +
2053 + // Special path for hydration
2054 // If we're currently hydrating, try to hydrate this boundary.
2055 tryToClaimNextHydratableInstance(workInProgress);
2056 // This could've been a dehydrated suspense component.
@@ -2129,161 +2131,65 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
2131 } else {
2132 // This is an update.
2133
2132 - // If the current fiber has a SuspenseState, that means it's already showing
2133 - // a fallback.
2134 + // Special path for hydration
2135 const prevState: null | SuspenseState = current.memoizedState;
2136 if (prevState !== null) {
2136 - // The current tree is already showing a fallback
2137 -
2138 - // Special path for hydration
2137 const dehydrated = prevState.dehydrated;
2138 if (dehydrated !== null) {
2141 - if (!didSuspend) {
2142 - return updateDehydratedSuspenseComponent(
2143 - current,
2144 - workInProgress,
2145 - dehydrated,
2146 - prevState,
2147 - renderLanes,
2148 - );
2149 - } else if (workInProgress.flags & ForceClientRender) {
2150 - // Something errored during hydration. Try again without hydrating.
2151 - workInProgress.flags &= ~ForceClientRender;
2152 - return retrySuspenseComponentWithoutHydrating(
2153 - current,
2154 - workInProgress,
2155 - renderLanes,
2156 - new Error(
2157 - 'There was an error while hydrating this Suspense boundary. ' +
2158 - 'Switched to client rendering.',
2159 - ),
2160 - );
2161 - } else if (
2162 - (workInProgress.memoizedState: null | SuspenseState) !== null
2163 - ) {
2164 - // Something suspended and we should still be in dehydrated mode.
2165 - // Leave the existing child in place.
2166 - workInProgress.child = current.child;
2167 - // The dehydrated completion pass expects this flag to be there
2168 - // but the normal suspense pass doesn't.
2169 - workInProgress.flags |= DidCapture;
2170 - return null;
2171 - } else {
2172 - // Suspended but we should no longer be in dehydrated mode.
2173 - // Therefore we now have to render the fallback.
2174 - const nextPrimaryChildren = nextProps.children;
2175 - const nextFallbackChildren = nextProps.fallback;
2176 - const fallbackChildFragment = mountSuspenseFallbackAfterRetryWithoutHydrating(
2177 - current,
2178 - workInProgress,
2179 - nextPrimaryChildren,
2180 - nextFallbackChildren,
2181 - renderLanes,
2182 - );
2183 - const primaryChildFragment: Fiber = (workInProgress.child: any);
2184 - primaryChildFragment.memoizedState = mountSuspenseOffscreenState(
2185 - renderLanes,
2186 - );
2187 - workInProgress.memoizedState = SUSPENDED_MARKER;
2188 - return fallbackChildFragment;
2189 - }
2190 - }
2191 -
2192 - if (showFallback) {
2193 - const nextFallbackChildren = nextProps.fallback;
2194 - const nextPrimaryChildren = nextProps.children;
2195 - const fallbackChildFragment = updateSuspenseFallbackChildren(
2139 + return updateDehydratedSuspenseComponent(
2140 current,
2141 workInProgress,
2198 - nextPrimaryChildren,
2199 - nextFallbackChildren,
2200 - renderLanes,
2201 - );
2202 - const primaryChildFragment: Fiber = (workInProgress.child: any);
2203 - const prevOffscreenState: OffscreenState | null = (current.child: any)
2204 - .memoizedState;
2205 - primaryChildFragment.memoizedState =
2206 - prevOffscreenState === null
2207 - ? mountSuspenseOffscreenState(renderLanes)
2208 - : updateSuspenseOffscreenState(prevOffscreenState, renderLanes);
2209 - if (enableTransitionTracing) {
2210 - const currentTransitions = getSuspendedTransitions();
2211 - if (currentTransitions !== null) {
2212 - const primaryChildUpdateQueue: OffscreenQueue = {
2213 - transitions: currentTransitions,
2214 - };
2215 - primaryChildFragment.updateQueue = primaryChildUpdateQueue;
2216 - }
2217 - }
2218 - primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
2219 - current,
2220 - renderLanes,
2221 - );
2222 - workInProgress.memoizedState = SUSPENDED_MARKER;
2223 - return fallbackChildFragment;
2224 - } else {
2225 - const nextPrimaryChildren = nextProps.children;
2226 - const primaryChildFragment = updateSuspensePrimaryChildren(
2227 - current,
2228 - workInProgress,
2229 - nextPrimaryChildren,
2142 + didSuspend,
2143 + nextProps,
2144 + dehydrated,
2145 + prevState,
2146 renderLanes,
2147 );
2232 - workInProgress.memoizedState = null;
2233 - return primaryChildFragment;
2148 }
2235 - } else {
2236 - // The current tree is not already showing a fallback.
2237 - if (showFallback) {
2238 - // Timed out.
2239 - const nextFallbackChildren = nextProps.fallback;
2240 - const nextPrimaryChildren = nextProps.children;
2241 - const fallbackChildFragment = updateSuspenseFallbackChildren(
2242 - current,
2243 - workInProgress,
2244 - nextPrimaryChildren,
2245 - nextFallbackChildren,
2246 - renderLanes,
2247 - );
2248 - const primaryChildFragment: Fiber = (workInProgress.child: any);
2249 - const prevOffscreenState: OffscreenState | null = (current.child: any)
2250 - .memoizedState;
2251 - primaryChildFragment.memoizedState =
2252 - prevOffscreenState === null
2253 - ? mountSuspenseOffscreenState(renderLanes)
2254 - : updateSuspenseOffscreenState(prevOffscreenState, renderLanes);
2255 - primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
2256 - current,
2257 - renderLanes,
2258 - );
2149 + }
2150
2260 - if (enableTransitionTracing) {
2261 - const currentTransitions = getSuspendedTransitions();
2262 - if (currentTransitions !== null) {
2263 - const primaryChildUpdateQueue: OffscreenQueue = {
2264 - transitions: currentTransitions,
2265 - };
2266 - primaryChildFragment.updateQueue = primaryChildUpdateQueue;
2267 - }
2151 + if (showFallback) {
2152 + const nextFallbackChildren = nextProps.fallback;
2153 + const nextPrimaryChildren = nextProps.children;
2154 + const fallbackChildFragment = updateSuspenseFallbackChildren(
2155 + current,
2156 + workInProgress,
2157 + nextPrimaryChildren,
2158 + nextFallbackChildren,
2159 + renderLanes,
2160 + );
2161 + const primaryChildFragment: Fiber = (workInProgress.child: any);
2162 + const prevOffscreenState: OffscreenState | null = (current.child: any)
2163 + .memoizedState;
2164 + primaryChildFragment.memoizedState =
2165 + prevOffscreenState === null
2166 + ? mountSuspenseOffscreenState(renderLanes)
2167 + : updateSuspenseOffscreenState(prevOffscreenState, renderLanes);
2168 + if (enableTransitionTracing) {
2169 + const currentTransitions = getSuspendedTransitions();
2170 + if (currentTransitions !== null) {
2171 + const primaryChildUpdateQueue: OffscreenQueue = {
2172 + transitions: currentTransitions,
2173 + };
2174 + primaryChildFragment.updateQueue = primaryChildUpdateQueue;
2175 }
2269 -
2270 - // Skip the primary children, and continue working on the
2271 - // fallback children.
2272 - workInProgress.memoizedState = SUSPENDED_MARKER;
2273 - return fallbackChildFragment;
2274 - } else {
2275 - // Still haven't timed out. Continue rendering the children, like we
2276 - // normally do.
2277 - const nextPrimaryChildren = nextProps.children;
2278 - const primaryChildFragment = updateSuspensePrimaryChildren(
2279 - current,
2280 - workInProgress,
2281 - nextPrimaryChildren,
2282 - renderLanes,
2283 - );
2284 - workInProgress.memoizedState = null;
2285 - return primaryChildFragment;
2176 }
2177 + primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
2178 + current,
2179 + renderLanes,
2180 + );
2181 + workInProgress.memoizedState = SUSPENDED_MARKER;
2182 + return fallbackChildFragment;
2183 + } else {
2184 + const nextPrimaryChildren = nextProps.children;
2185 + const primaryChildFragment = updateSuspensePrimaryChildren(
2186 + current,
2187 + workInProgress,
2188 + nextPrimaryChildren,
2189 + renderLanes,
2190 + );
2191 + workInProgress.memoizedState = null;
2192 + return primaryChildFragment;
2193 }
2194 }
2195 }
@@ -2637,146 +2543,194 @@ function mountDehydratedSuspenseComponent(
2543 function updateDehydratedSuspenseComponent(
2544 current: Fiber,
2545 workInProgress: Fiber,
2546 + didSuspend: boolean,
2547 + nextProps: any,
2548 suspenseInstance: SuspenseInstance,
2549 suspenseState: SuspenseState,
2550 renderLanes: Lanes,
2551 ): null | Fiber {
2644 - // We should never be hydrating at this point because it is the first pass,
2645 - // but after we've already committed once.
2646 - warnIfHydrating();
2552 + if (!didSuspend) {
2553 + // This is the first render pass. Attempt to hydrate.
2554
2648 - if ((workInProgress.mode & ConcurrentMode) === NoMode) {
2649 - return retrySuspenseComponentWithoutHydrating(
2650 - current,
2651 - workInProgress,
2652 - renderLanes,
2653 - // TODO: When we delete legacy mode, we should make this error argument
2654 - // required — every concurrent mode path that causes hydration to
2655 - // de-opt to client rendering should have an error message.
2656 - null,
2657 - );
2658 - }
2555 + // We should never be hydrating at this point because it is the first pass,
2556 + // but after we've already committed once.
2557 + warnIfHydrating();
2558
2660 - if (isSuspenseInstanceFallback(suspenseInstance)) {
2661 - // This boundary is in a permanent fallback state. In this case, we'll never
2662 - // get an update and we'll never be able to hydrate the final content. Let's just try the
2663 - // client side render instead.
2664 - return retrySuspenseComponentWithoutHydrating(
2665 - current,
2666 - workInProgress,
2667 - renderLanes,
2668 - // TODO: The server should serialize the error message so we can log it
2669 - // here on the client. Or, in production, a hash/id that corresponds to
2670 - // the error.
2671 - new Error(
2672 - 'The server could not finish this Suspense boundary, likely ' +
2673 - 'due to an error during server rendering. Switched to ' +
2674 - 'client rendering.',
2675 - ),
2676 - );
2677 - }
2559 + if ((workInProgress.mode & ConcurrentMode) === NoMode) {
2560 + return retrySuspenseComponentWithoutHydrating(
2561 + current,
2562 + workInProgress,
2563 + renderLanes,
2564 + // TODO: When we delete legacy mode, we should make this error argument
2565 + // required — every concurrent mode path that causes hydration to
2566 + // de-opt to client rendering should have an error message.
2567 + null,
2568 + );
2569 + }
2570
2679 - if (
2680 - enableLazyContextPropagation &&
2681 - // TODO: Factoring is a little weird, since we check this right below, too.
2682 - // But don't want to re-arrange the if-else chain until/unless this
2683 - // feature lands.
2684 - !didReceiveUpdate
2685 - ) {
2686 - // We need to check if any children have context before we decide to bail
2687 - // out, so propagate the changes now.
2688 - lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
2689 - }
2690 -
2691 - // We use lanes to indicate that a child might depend on context, so if
2692 - // any context has changed, we need to treat is as if the input might have changed.
2693 - const hasContextChanged = includesSomeLane(renderLanes, current.childLanes);
2694 - if (didReceiveUpdate || hasContextChanged) {
2695 - // This boundary has changed since the first render. This means that we are now unable to
2696 - // hydrate it. We might still be able to hydrate it using a higher priority lane.
2697 - const root = getWorkInProgressRoot();
2698 - if (root !== null) {
2699 - const attemptHydrationAtLane = getBumpedLaneForHydration(
2700 - root,
2571 + if (isSuspenseInstanceFallback(suspenseInstance)) {
2572 + // This boundary is in a permanent fallback state. In this case, we'll never
2573 + // get an update and we'll never be able to hydrate the final content. Let's just try the
2574 + // client side render instead.
2575 + return retrySuspenseComponentWithoutHydrating(
2576 + current,
2577 + workInProgress,
2578 renderLanes,
2579 + // TODO: The server should serialize the error message so we can log it
2580 + // here on the client. Or, in production, a hash/id that corresponds to
2581 + // the error.
2582 + new Error(
2583 + 'The server could not finish this Suspense boundary, likely ' +
2584 + 'due to an error during server rendering. Switched to ' +
2585 + 'client rendering.',
2586 + ),
2587 );
2703 - if (
2704 - attemptHydrationAtLane !== NoLane &&
2705 - attemptHydrationAtLane !== suspenseState.retryLane
2706 - ) {
2707 - // Intentionally mutating since this render will get interrupted. This
2708 - // is one of the very rare times where we mutate the current tree
2709 - // during the render phase.
2710 - suspenseState.retryLane = attemptHydrationAtLane;
2711 - // TODO: Ideally this would inherit the event time of the current render
2712 - const eventTime = NoTimestamp;
2713 - scheduleUpdateOnFiber(current, attemptHydrationAtLane, eventTime);
2714 - } else {
2715 - // We have already tried to ping at a higher priority than we're rendering with
2716 - // so if we got here, we must have failed to hydrate at those levels. We must
2717 - // now give up. Instead, we're going to delete the whole subtree and instead inject
2718 - // a new real Suspense boundary to take its place, which may render content
2719 - // or fallback. This might suspend for a while and if it does we might still have
2720 - // an opportunity to hydrate before this pass commits.
2721 - }
2722 - }
2723 -
2724 - // If we have scheduled higher pri work above, this will probably just abort the render
2725 - // since we now have higher priority work, but in case it doesn't, we need to prepare to
2726 - // render something, if we time out. Even if that requires us to delete everything and
2727 - // skip hydration.
2728 - // Delay having to do this as long as the suspense timeout allows us.
2729 - renderDidSuspendDelayIfPossible();
2730 - return retrySuspenseComponentWithoutHydrating(
2731 - current,
2732 - workInProgress,
2733 - renderLanes,
2734 - new Error(
2735 - 'This Suspense boundary received an update before it finished ' +
2736 - 'hydrating. This caused the boundary to switch to client rendering. ' +
2737 - 'The usual way to fix this is to wrap the original update ' +
2738 - 'in startTransition.',
2739 - ),
2740 - );
2741 - } else if (isSuspenseInstancePending(suspenseInstance)) {
2742 - // This component is still pending more data from the server, so we can't hydrate its
2743 - // content. We treat it as if this component suspended itself. It might seem as if
2744 - // we could just try to render it client-side instead. However, this will perform a
2745 - // lot of unnecessary work and is unlikely to complete since it often will suspend
2746 - // on missing data anyway. Additionally, the server might be able to render more
2747 - // than we can on the client yet. In that case we'd end up with more fallback states
2748 - // on the client than if we just leave it alone. If the server times out or errors
2749 - // these should update this boundary to the permanent Fallback state instead.
2750 - // Mark it as having captured (i.e. suspended).
2751 - workInProgress.flags |= DidCapture;
2752 - // Leave the child in place. I.e. the dehydrated fragment.
2753 - workInProgress.child = current.child;
2754 - // Register a callback to retry this boundary once the server has sent the result.
2755 - const retry = retryDehydratedSuspenseBoundary.bind(null, current);
2756 - registerSuspenseInstanceRetry(suspenseInstance, retry);
2757 - return null;
2588 + }
2589 +
2590 + if (
2591 + enableLazyContextPropagation &&
2592 + // TODO: Factoring is a little weird, since we check this right below, too.
2593 + // But don't want to re-arrange the if-else chain until/unless this
2594 + // feature lands.
2595 + !didReceiveUpdate
2596 + ) {
2597 + // We need to check if any children have context before we decide to bail
2598 + // out, so propagate the changes now.
2599 + lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
2600 + }
2601 +
2602 + // We use lanes to indicate that a child might depend on context, so if
2603 + // any context has changed, we need to treat is as if the input might have changed.
2604 + const hasContextChanged = includesSomeLane(renderLanes, current.childLanes);
2605 + if (didReceiveUpdate || hasContextChanged) {
2606 + // This boundary has changed since the first render. This means that we are now unable to
2607 + // hydrate it. We might still be able to hydrate it using a higher priority lane.
2608 + const root = getWorkInProgressRoot();
2609 + if (root !== null) {
2610 + const attemptHydrationAtLane = getBumpedLaneForHydration(
2611 + root,
2612 + renderLanes,
2613 + );
2614 + if (
2615 + attemptHydrationAtLane !== NoLane &&
2616 + attemptHydrationAtLane !== suspenseState.retryLane
2617 + ) {
2618 + // Intentionally mutating since this render will get interrupted. This
2619 + // is one of the very rare times where we mutate the current tree
2620 + // during the render phase.
2621 + suspenseState.retryLane = attemptHydrationAtLane;
2622 + // TODO: Ideally this would inherit the event time of the current render
2623 + const eventTime = NoTimestamp;
2624 + scheduleUpdateOnFiber(current, attemptHydrationAtLane, eventTime);
2625 + } else {
2626 + // We have already tried to ping at a higher priority than we're rendering with
2627 + // so if we got here, we must have failed to hydrate at those levels. We must
2628 + // now give up. Instead, we're going to delete the whole subtree and instead inject
2629 + // a new real Suspense boundary to take its place, which may render content
2630 + // or fallback. This might suspend for a while and if it does we might still have
2631 + // an opportunity to hydrate before this pass commits.
2632 + }
2633 + }
2634 +
2635 + // If we have scheduled higher pri work above, this will probably just abort the render
2636 + // since we now have higher priority work, but in case it doesn't, we need to prepare to
2637 + // render something, if we time out. Even if that requires us to delete everything and
2638 + // skip hydration.
2639 + // Delay having to do this as long as the suspense timeout allows us.
2640 + renderDidSuspendDelayIfPossible();
2641 + return retrySuspenseComponentWithoutHydrating(
2642 + current,
2643 + workInProgress,
2644 + renderLanes,
2645 + new Error(
2646 + 'This Suspense boundary received an update before it finished ' +
2647 + 'hydrating. This caused the boundary to switch to client rendering. ' +
2648 + 'The usual way to fix this is to wrap the original update ' +
2649 + 'in startTransition.',
2650 + ),
2651 + );
2652 + } else if (isSuspenseInstancePending(suspenseInstance)) {
2653 + // This component is still pending more data from the server, so we can't hydrate its
2654 + // content. We treat it as if this component suspended itself. It might seem as if
2655 + // we could just try to render it client-side instead. However, this will perform a
2656 + // lot of unnecessary work and is unlikely to complete since it often will suspend
2657 + // on missing data anyway. Additionally, the server might be able to render more
2658 + // than we can on the client yet. In that case we'd end up with more fallback states
2659 + // on the client than if we just leave it alone. If the server times out or errors
2660 + // these should update this boundary to the permanent Fallback state instead.
2661 + // Mark it as having captured (i.e. suspended).
2662 + workInProgress.flags |= DidCapture;
2663 + // Leave the child in place. I.e. the dehydrated fragment.
2664 + workInProgress.child = current.child;
2665 + // Register a callback to retry this boundary once the server has sent the result.
2666 + const retry = retryDehydratedSuspenseBoundary.bind(null, current);
2667 + registerSuspenseInstanceRetry(suspenseInstance, retry);
2668 + return null;
2669 + } else {
2670 + // This is the first attempt.
2671 + reenterHydrationStateFromDehydratedSuspenseInstance(
2672 + workInProgress,
2673 + suspenseInstance,
2674 + suspenseState.treeContext,
2675 + );
2676 + const primaryChildren = nextProps.children;
2677 + const primaryChildFragment = mountSuspensePrimaryChildren(
2678 + workInProgress,
2679 + primaryChildren,
2680 + renderLanes,
2681 + );
2682 + // Mark the children as hydrating. This is a fast path to know whether this
2683 + // tree is part of a hydrating tree. This is used to determine if a child
2684 + // node has fully mounted yet, and for scheduling event replaying.
2685 + // Conceptually this is similar to Placement in that a new subtree is
2686 + // inserted into the React tree here. It just happens to not need DOM
2687 + // mutations because it already exists.
2688 + primaryChildFragment.flags |= Hydrating;
2689 + return primaryChildFragment;
2690 + }
2691 } else {
2759 - // This is the first attempt.
2760 - reenterHydrationStateFromDehydratedSuspenseInstance(
2761 - workInProgress,
2762 - suspenseInstance,
2763 - suspenseState.treeContext,
2764 - );
2765 - const nextProps = workInProgress.pendingProps;
2766 - const primaryChildren = nextProps.children;
2767 - const primaryChildFragment = mountSuspensePrimaryChildren(
2768 - workInProgress,
2769 - primaryChildren,
2770 - renderLanes,
2771 - );
2772 - // Mark the children as hydrating. This is a fast path to know whether this
2773 - // tree is part of a hydrating tree. This is used to determine if a child
2774 - // node has fully mounted yet, and for scheduling event replaying.
2775 - // Conceptually this is similar to Placement in that a new subtree is
2776 - // inserted into the React tree here. It just happens to not need DOM
2777 - // mutations because it already exists.
2778 - primaryChildFragment.flags |= Hydrating;
2779 - return primaryChildFragment;
2692 + // This is the second render pass. We already attempted to hydrated, but
2693 + // something either suspended or errored.
2694 +
2695 + if (workInProgress.flags & ForceClientRender) {
2696 + // Something errored during hydration. Try again without hydrating.
2697 + workInProgress.flags &= ~ForceClientRender;
2698 + return retrySuspenseComponentWithoutHydrating(
2699 + current,
2700 + workInProgress,
2701 + renderLanes,
2702 + new Error(
2703 + 'There was an error while hydrating this Suspense boundary. ' +
2704 + 'Switched to client rendering.',
2705 + ),
2706 + );
2707 + } else if ((workInProgress.memoizedState: null | SuspenseState) !== null) {
2708 + // Something suspended and we should still be in dehydrated mode.
2709 + // Leave the existing child in place.
2710 + workInProgress.child = current.child;
2711 + // The dehydrated completion pass expects this flag to be there
2712 + // but the normal suspense pass doesn't.
2713 + workInProgress.flags |= DidCapture;
2714 + return null;
2715 + } else {
2716 + // Suspended but we should no longer be in dehydrated mode.
2717 + // Therefore we now have to render the fallback.
2718 + const nextPrimaryChildren = nextProps.children;
2719 + const nextFallbackChildren = nextProps.fallback;
2720 + const fallbackChildFragment = mountSuspenseFallbackAfterRetryWithoutHydrating(
2721 + current,
2722 + workInProgress,
2723 + nextPrimaryChildren,
2724 + nextFallbackChildren,
2725 + renderLanes,
2726 + );
2727 + const primaryChildFragment: Fiber = (workInProgress.child: any);
2728 + primaryChildFragment.memoizedState = mountSuspenseOffscreenState(
2729 + renderLanes,
2730 + );
2731 + workInProgress.memoizedState = SUSPENDED_MARKER;
2732 + return fallbackChildFragment;
2733 + }
2734 }
2735 }
2736
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+227 -273
@@ -2049,6 +2049,8 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
2049 // a stack.
2050 if (current === null) {
2051 // Initial mount
2052 +
2053 + // Special path for hydration
2054 // If we're currently hydrating, try to hydrate this boundary.
2055 tryToClaimNextHydratableInstance(workInProgress);
2056 // This could've been a dehydrated suspense component.
@@ -2129,161 +2131,65 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
2131 } else {
2132 // This is an update.
2133
2132 - // If the current fiber has a SuspenseState, that means it's already showing
2133 - // a fallback.
2134 + // Special path for hydration
2135 const prevState: null | SuspenseState = current.memoizedState;
2136 if (prevState !== null) {
2136 - // The current tree is already showing a fallback
2137 -
2138 - // Special path for hydration
2137 const dehydrated = prevState.dehydrated;
2138 if (dehydrated !== null) {
2141 - if (!didSuspend) {
2142 - return updateDehydratedSuspenseComponent(
2143 - current,
2144 - workInProgress,
2145 - dehydrated,
2146 - prevState,
2147 - renderLanes,
2148 - );
2149 - } else if (workInProgress.flags & ForceClientRender) {
2150 - // Something errored during hydration. Try again without hydrating.
2151 - workInProgress.flags &= ~ForceClientRender;
2152 - return retrySuspenseComponentWithoutHydrating(
2153 - current,
2154 - workInProgress,
2155 - renderLanes,
2156 - new Error(
2157 - 'There was an error while hydrating this Suspense boundary. ' +
2158 - 'Switched to client rendering.',
2159 - ),
2160 - );
2161 - } else if (
2162 - (workInProgress.memoizedState: null | SuspenseState) !== null
2163 - ) {
2164 - // Something suspended and we should still be in dehydrated mode.
2165 - // Leave the existing child in place.
2166 - workInProgress.child = current.child;
2167 - // The dehydrated completion pass expects this flag to be there
2168 - // but the normal suspense pass doesn't.
2169 - workInProgress.flags |= DidCapture;
2170 - return null;
2171 - } else {
2172 - // Suspended but we should no longer be in dehydrated mode.
2173 - // Therefore we now have to render the fallback.
2174 - const nextPrimaryChildren = nextProps.children;
2175 - const nextFallbackChildren = nextProps.fallback;
2176 - const fallbackChildFragment = mountSuspenseFallbackAfterRetryWithoutHydrating(
2177 - current,
2178 - workInProgress,
2179 - nextPrimaryChildren,
2180 - nextFallbackChildren,
2181 - renderLanes,
2182 - );
2183 - const primaryChildFragment: Fiber = (workInProgress.child: any);
2184 - primaryChildFragment.memoizedState = mountSuspenseOffscreenState(
2185 - renderLanes,
2186 - );
2187 - workInProgress.memoizedState = SUSPENDED_MARKER;
2188 - return fallbackChildFragment;
2189 - }
2190 - }
2191 -
2192 - if (showFallback) {
2193 - const nextFallbackChildren = nextProps.fallback;
2194 - const nextPrimaryChildren = nextProps.children;
2195 - const fallbackChildFragment = updateSuspenseFallbackChildren(
2139 + return updateDehydratedSuspenseComponent(
2140 current,
2141 workInProgress,
2198 - nextPrimaryChildren,
2199 - nextFallbackChildren,
2200 - renderLanes,
2201 - );
2202 - const primaryChildFragment: Fiber = (workInProgress.child: any);
2203 - const prevOffscreenState: OffscreenState | null = (current.child: any)
2204 - .memoizedState;
2205 - primaryChildFragment.memoizedState =
2206 - prevOffscreenState === null
2207 - ? mountSuspenseOffscreenState(renderLanes)
2208 - : updateSuspenseOffscreenState(prevOffscreenState, renderLanes);
2209 - if (enableTransitionTracing) {
2210 - const currentTransitions = getSuspendedTransitions();
2211 - if (currentTransitions !== null) {
2212 - const primaryChildUpdateQueue: OffscreenQueue = {
2213 - transitions: currentTransitions,
2214 - };
2215 - primaryChildFragment.updateQueue = primaryChildUpdateQueue;
2216 - }
2217 - }
2218 - primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
2219 - current,
2220 - renderLanes,
2221 - );
2222 - workInProgress.memoizedState = SUSPENDED_MARKER;
2223 - return fallbackChildFragment;
2224 - } else {
2225 - const nextPrimaryChildren = nextProps.children;
2226 - const primaryChildFragment = updateSuspensePrimaryChildren(
2227 - current,
2228 - workInProgress,
2229 - nextPrimaryChildren,
2142 + didSuspend,
2143 + nextProps,
2144 + dehydrated,
2145 + prevState,
2146 renderLanes,
2147 );
2232 - workInProgress.memoizedState = null;
2233 - return primaryChildFragment;
2148 }
2235 - } else {
2236 - // The current tree is not already showing a fallback.
2237 - if (showFallback) {
2238 - // Timed out.
2239 - const nextFallbackChildren = nextProps.fallback;
2240 - const nextPrimaryChildren = nextProps.children;
2241 - const fallbackChildFragment = updateSuspenseFallbackChildren(
2242 - current,
2243 - workInProgress,
2244 - nextPrimaryChildren,
2245 - nextFallbackChildren,
2246 - renderLanes,
2247 - );
2248 - const primaryChildFragment: Fiber = (workInProgress.child: any);
2249 - const prevOffscreenState: OffscreenState | null = (current.child: any)
2250 - .memoizedState;
2251 - primaryChildFragment.memoizedState =
2252 - prevOffscreenState === null
2253 - ? mountSuspenseOffscreenState(renderLanes)
2254 - : updateSuspenseOffscreenState(prevOffscreenState, renderLanes);
2255 - primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
2256 - current,
2257 - renderLanes,
2258 - );
2149 + }
2150
2260 - if (enableTransitionTracing) {
2261 - const currentTransitions = getSuspendedTransitions();
2262 - if (currentTransitions !== null) {
2263 - const primaryChildUpdateQueue: OffscreenQueue = {
2264 - transitions: currentTransitions,
2265 - };
2266 - primaryChildFragment.updateQueue = primaryChildUpdateQueue;
2267 - }
2151 + if (showFallback) {
2152 + const nextFallbackChildren = nextProps.fallback;
2153 + const nextPrimaryChildren = nextProps.children;
2154 + const fallbackChildFragment = updateSuspenseFallbackChildren(
2155 + current,
2156 + workInProgress,
2157 + nextPrimaryChildren,
2158 + nextFallbackChildren,
2159 + renderLanes,
2160 + );
2161 + const primaryChildFragment: Fiber = (workInProgress.child: any);
2162 + const prevOffscreenState: OffscreenState | null = (current.child: any)
2163 + .memoizedState;
2164 + primaryChildFragment.memoizedState =
2165 + prevOffscreenState === null
2166 + ? mountSuspenseOffscreenState(renderLanes)
2167 + : updateSuspenseOffscreenState(prevOffscreenState, renderLanes);
2168 + if (enableTransitionTracing) {
2169 + const currentTransitions = getSuspendedTransitions();
2170 + if (currentTransitions !== null) {
2171 + const primaryChildUpdateQueue: OffscreenQueue = {
2172 + transitions: currentTransitions,
2173 + };
2174 + primaryChildFragment.updateQueue = primaryChildUpdateQueue;
2175 }
2269 -
2270 - // Skip the primary children, and continue working on the
2271 - // fallback children.
2272 - workInProgress.memoizedState = SUSPENDED_MARKER;
2273 - return fallbackChildFragment;
2274 - } else {
2275 - // Still haven't timed out. Continue rendering the children, like we
2276 - // normally do.
2277 - const nextPrimaryChildren = nextProps.children;
2278 - const primaryChildFragment = updateSuspensePrimaryChildren(
2279 - current,
2280 - workInProgress,
2281 - nextPrimaryChildren,
2282 - renderLanes,
2283 - );
2284 - workInProgress.memoizedState = null;
2285 - return primaryChildFragment;
2176 }
2177 + primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
2178 + current,
2179 + renderLanes,
2180 + );
2181 + workInProgress.memoizedState = SUSPENDED_MARKER;
2182 + return fallbackChildFragment;
2183 + } else {
2184 + const nextPrimaryChildren = nextProps.children;
2185 + const primaryChildFragment = updateSuspensePrimaryChildren(
2186 + current,
2187 + workInProgress,
2188 + nextPrimaryChildren,
2189 + renderLanes,
2190 + );
2191 + workInProgress.memoizedState = null;
2192 + return primaryChildFragment;
2193 }
2194 }
2195 }
@@ -2637,146 +2543,194 @@ function mountDehydratedSuspenseComponent(
2543 function updateDehydratedSuspenseComponent(
2544 current: Fiber,
2545 workInProgress: Fiber,
2546 + didSuspend: boolean,
2547 + nextProps: any,
2548 suspenseInstance: SuspenseInstance,
2549 suspenseState: SuspenseState,
2550 renderLanes: Lanes,
2551 ): null | Fiber {
2644 - // We should never be hydrating at this point because it is the first pass,
2645 - // but after we've already committed once.
2646 - warnIfHydrating();
2552 + if (!didSuspend) {
2553 + // This is the first render pass. Attempt to hydrate.
2554
2648 - if ((workInProgress.mode & ConcurrentMode) === NoMode) {
2649 - return retrySuspenseComponentWithoutHydrating(
2650 - current,
2651 - workInProgress,
2652 - renderLanes,
2653 - // TODO: When we delete legacy mode, we should make this error argument
2654 - // required — every concurrent mode path that causes hydration to
2655 - // de-opt to client rendering should have an error message.
2656 - null,
2657 - );
2658 - }
2555 + // We should never be hydrating at this point because it is the first pass,
2556 + // but after we've already committed once.
2557 + warnIfHydrating();
2558
2660 - if (isSuspenseInstanceFallback(suspenseInstance)) {
2661 - // This boundary is in a permanent fallback state. In this case, we'll never
2662 - // get an update and we'll never be able to hydrate the final content. Let's just try the
2663 - // client side render instead.
2664 - return retrySuspenseComponentWithoutHydrating(
2665 - current,
2666 - workInProgress,
2667 - renderLanes,
2668 - // TODO: The server should serialize the error message so we can log it
2669 - // here on the client. Or, in production, a hash/id that corresponds to
2670 - // the error.
2671 - new Error(
2672 - 'The server could not finish this Suspense boundary, likely ' +
2673 - 'due to an error during server rendering. Switched to ' +
2674 - 'client rendering.',
2675 - ),
2676 - );
2677 - }
2559 + if ((workInProgress.mode & ConcurrentMode) === NoMode) {
2560 + return retrySuspenseComponentWithoutHydrating(
2561 + current,
2562 + workInProgress,
2563 + renderLanes,
2564 + // TODO: When we delete legacy mode, we should make this error argument
2565 + // required — every concurrent mode path that causes hydration to
2566 + // de-opt to client rendering should have an error message.
2567 + null,
2568 + );
2569 + }
2570
2679 - if (
2680 - enableLazyContextPropagation &&
2681 - // TODO: Factoring is a little weird, since we check this right below, too.
2682 - // But don't want to re-arrange the if-else chain until/unless this
2683 - // feature lands.
2684 - !didReceiveUpdate
2685 - ) {
2686 - // We need to check if any children have context before we decide to bail
2687 - // out, so propagate the changes now.
2688 - lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
2689 - }
2690 -
2691 - // We use lanes to indicate that a child might depend on context, so if
2692 - // any context has changed, we need to treat is as if the input might have changed.
2693 - const hasContextChanged = includesSomeLane(renderLanes, current.childLanes);
2694 - if (didReceiveUpdate || hasContextChanged) {
2695 - // This boundary has changed since the first render. This means that we are now unable to
2696 - // hydrate it. We might still be able to hydrate it using a higher priority lane.
2697 - const root = getWorkInProgressRoot();
2698 - if (root !== null) {
2699 - const attemptHydrationAtLane = getBumpedLaneForHydration(
2700 - root,
2571 + if (isSuspenseInstanceFallback(suspenseInstance)) {
2572 + // This boundary is in a permanent fallback state. In this case, we'll never
2573 + // get an update and we'll never be able to hydrate the final content. Let's just try the
2574 + // client side render instead.
2575 + return retrySuspenseComponentWithoutHydrating(
2576 + current,
2577 + workInProgress,
2578 renderLanes,
2579 + // TODO: The server should serialize the error message so we can log it
2580 + // here on the client. Or, in production, a hash/id that corresponds to
2581 + // the error.
2582 + new Error(
2583 + 'The server could not finish this Suspense boundary, likely ' +
2584 + 'due to an error during server rendering. Switched to ' +
2585 + 'client rendering.',
2586 + ),
2587 );
2703 - if (
2704 - attemptHydrationAtLane !== NoLane &&
2705 - attemptHydrationAtLane !== suspenseState.retryLane
2706 - ) {
2707 - // Intentionally mutating since this render will get interrupted. This
2708 - // is one of the very rare times where we mutate the current tree
2709 - // during the render phase.
2710 - suspenseState.retryLane = attemptHydrationAtLane;
2711 - // TODO: Ideally this would inherit the event time of the current render
2712 - const eventTime = NoTimestamp;
2713 - scheduleUpdateOnFiber(current, attemptHydrationAtLane, eventTime);
2714 - } else {
2715 - // We have already tried to ping at a higher priority than we're rendering with
2716 - // so if we got here, we must have failed to hydrate at those levels. We must
2717 - // now give up. Instead, we're going to delete the whole subtree and instead inject
2718 - // a new real Suspense boundary to take its place, which may render content
2719 - // or fallback. This might suspend for a while and if it does we might still have
2720 - // an opportunity to hydrate before this pass commits.
2721 - }
2722 - }
2723 -
2724 - // If we have scheduled higher pri work above, this will probably just abort the render
2725 - // since we now have higher priority work, but in case it doesn't, we need to prepare to
2726 - // render something, if we time out. Even if that requires us to delete everything and
2727 - // skip hydration.
2728 - // Delay having to do this as long as the suspense timeout allows us.
2729 - renderDidSuspendDelayIfPossible();
2730 - return retrySuspenseComponentWithoutHydrating(
2731 - current,
2732 - workInProgress,
2733 - renderLanes,
2734 - new Error(
2735 - 'This Suspense boundary received an update before it finished ' +
2736 - 'hydrating. This caused the boundary to switch to client rendering. ' +
2737 - 'The usual way to fix this is to wrap the original update ' +
2738 - 'in startTransition.',
2739 - ),
2740 - );
2741 - } else if (isSuspenseInstancePending(suspenseInstance)) {
2742 - // This component is still pending more data from the server, so we can't hydrate its
2743 - // content. We treat it as if this component suspended itself. It might seem as if
2744 - // we could just try to render it client-side instead. However, this will perform a
2745 - // lot of unnecessary work and is unlikely to complete since it often will suspend
2746 - // on missing data anyway. Additionally, the server might be able to render more
2747 - // than we can on the client yet. In that case we'd end up with more fallback states
2748 - // on the client than if we just leave it alone. If the server times out or errors
2749 - // these should update this boundary to the permanent Fallback state instead.
2750 - // Mark it as having captured (i.e. suspended).
2751 - workInProgress.flags |= DidCapture;
2752 - // Leave the child in place. I.e. the dehydrated fragment.
2753 - workInProgress.child = current.child;
2754 - // Register a callback to retry this boundary once the server has sent the result.
2755 - const retry = retryDehydratedSuspenseBoundary.bind(null, current);
2756 - registerSuspenseInstanceRetry(suspenseInstance, retry);
2757 - return null;
2588 + }
2589 +
2590 + if (
2591 + enableLazyContextPropagation &&
2592 + // TODO: Factoring is a little weird, since we check this right below, too.
2593 + // But don't want to re-arrange the if-else chain until/unless this
2594 + // feature lands.
2595 + !didReceiveUpdate
2596 + ) {
2597 + // We need to check if any children have context before we decide to bail
2598 + // out, so propagate the changes now.
2599 + lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
2600 + }
2601 +
2602 + // We use lanes to indicate that a child might depend on context, so if
2603 + // any context has changed, we need to treat is as if the input might have changed.
2604 + const hasContextChanged = includesSomeLane(renderLanes, current.childLanes);
2605 + if (didReceiveUpdate || hasContextChanged) {
2606 + // This boundary has changed since the first render. This means that we are now unable to
2607 + // hydrate it. We might still be able to hydrate it using a higher priority lane.
2608 + const root = getWorkInProgressRoot();
2609 + if (root !== null) {
2610 + const attemptHydrationAtLane = getBumpedLaneForHydration(
2611 + root,
2612 + renderLanes,
2613 + );
2614 + if (
2615 + attemptHydrationAtLane !== NoLane &&
2616 + attemptHydrationAtLane !== suspenseState.retryLane
2617 + ) {
2618 + // Intentionally mutating since this render will get interrupted. This
2619 + // is one of the very rare times where we mutate the current tree
2620 + // during the render phase.
2621 + suspenseState.retryLane = attemptHydrationAtLane;
2622 + // TODO: Ideally this would inherit the event time of the current render
2623 + const eventTime = NoTimestamp;
2624 + scheduleUpdateOnFiber(current, attemptHydrationAtLane, eventTime);
2625 + } else {
2626 + // We have already tried to ping at a higher priority than we're rendering with
2627 + // so if we got here, we must have failed to hydrate at those levels. We must
2628 + // now give up. Instead, we're going to delete the whole subtree and instead inject
2629 + // a new real Suspense boundary to take its place, which may render content
2630 + // or fallback. This might suspend for a while and if it does we might still have
2631 + // an opportunity to hydrate before this pass commits.
2632 + }
2633 + }
2634 +
2635 + // If we have scheduled higher pri work above, this will probably just abort the render
2636 + // since we now have higher priority work, but in case it doesn't, we need to prepare to
2637 + // render something, if we time out. Even if that requires us to delete everything and
2638 + // skip hydration.
2639 + // Delay having to do this as long as the suspense timeout allows us.
2640 + renderDidSuspendDelayIfPossible();
2641 + return retrySuspenseComponentWithoutHydrating(
2642 + current,
2643 + workInProgress,
2644 + renderLanes,
2645 + new Error(
2646 + 'This Suspense boundary received an update before it finished ' +
2647 + 'hydrating. This caused the boundary to switch to client rendering. ' +
2648 + 'The usual way to fix this is to wrap the original update ' +
2649 + 'in startTransition.',
2650 + ),
2651 + );
2652 + } else if (isSuspenseInstancePending(suspenseInstance)) {
2653 + // This component is still pending more data from the server, so we can't hydrate its
2654 + // content. We treat it as if this component suspended itself. It might seem as if
2655 + // we could just try to render it client-side instead. However, this will perform a
2656 + // lot of unnecessary work and is unlikely to complete since it often will suspend
2657 + // on missing data anyway. Additionally, the server might be able to render more
2658 + // than we can on the client yet. In that case we'd end up with more fallback states
2659 + // on the client than if we just leave it alone. If the server times out or errors
2660 + // these should update this boundary to the permanent Fallback state instead.
2661 + // Mark it as having captured (i.e. suspended).
2662 + workInProgress.flags |= DidCapture;
2663 + // Leave the child in place. I.e. the dehydrated fragment.
2664 + workInProgress.child = current.child;
2665 + // Register a callback to retry this boundary once the server has sent the result.
2666 + const retry = retryDehydratedSuspenseBoundary.bind(null, current);
2667 + registerSuspenseInstanceRetry(suspenseInstance, retry);
2668 + return null;
2669 + } else {
2670 + // This is the first attempt.
2671 + reenterHydrationStateFromDehydratedSuspenseInstance(
2672 + workInProgress,
2673 + suspenseInstance,
2674 + suspenseState.treeContext,
2675 + );
2676 + const primaryChildren = nextProps.children;
2677 + const primaryChildFragment = mountSuspensePrimaryChildren(
2678 + workInProgress,
2679 + primaryChildren,
2680 + renderLanes,
2681 + );
2682 + // Mark the children as hydrating. This is a fast path to know whether this
2683 + // tree is part of a hydrating tree. This is used to determine if a child
2684 + // node has fully mounted yet, and for scheduling event replaying.
2685 + // Conceptually this is similar to Placement in that a new subtree is
2686 + // inserted into the React tree here. It just happens to not need DOM
2687 + // mutations because it already exists.
2688 + primaryChildFragment.flags |= Hydrating;
2689 + return primaryChildFragment;
2690 + }
2691 } else {
2759 - // This is the first attempt.
2760 - reenterHydrationStateFromDehydratedSuspenseInstance(
2761 - workInProgress,
2762 - suspenseInstance,
2763 - suspenseState.treeContext,
2764 - );
2765 - const nextProps = workInProgress.pendingProps;
2766 - const primaryChildren = nextProps.children;
2767 - const primaryChildFragment = mountSuspensePrimaryChildren(
2768 - workInProgress,
2769 - primaryChildren,
2770 - renderLanes,
2771 - );
2772 - // Mark the children as hydrating. This is a fast path to know whether this
2773 - // tree is part of a hydrating tree. This is used to determine if a child
2774 - // node has fully mounted yet, and for scheduling event replaying.
2775 - // Conceptually this is similar to Placement in that a new subtree is
2776 - // inserted into the React tree here. It just happens to not need DOM
2777 - // mutations because it already exists.
2778 - primaryChildFragment.flags |= Hydrating;
2779 - return primaryChildFragment;
2692 + // This is the second render pass. We already attempted to hydrated, but
2693 + // something either suspended or errored.
2694 +
2695 + if (workInProgress.flags & ForceClientRender) {
2696 + // Something errored during hydration. Try again without hydrating.
2697 + workInProgress.flags &= ~ForceClientRender;
2698 + return retrySuspenseComponentWithoutHydrating(
2699 + current,
2700 + workInProgress,
2701 + renderLanes,
2702 + new Error(
2703 + 'There was an error while hydrating this Suspense boundary. ' +
2704 + 'Switched to client rendering.',
2705 + ),
2706 + );
2707 + } else if ((workInProgress.memoizedState: null | SuspenseState) !== null) {
2708 + // Something suspended and we should still be in dehydrated mode.
2709 + // Leave the existing child in place.
2710 + workInProgress.child = current.child;
2711 + // The dehydrated completion pass expects this flag to be there
2712 + // but the normal suspense pass doesn't.
2713 + workInProgress.flags |= DidCapture;
2714 + return null;
2715 + } else {
2716 + // Suspended but we should no longer be in dehydrated mode.
2717 + // Therefore we now have to render the fallback.
2718 + const nextPrimaryChildren = nextProps.children;
2719 + const nextFallbackChildren = nextProps.fallback;
2720 + const fallbackChildFragment = mountSuspenseFallbackAfterRetryWithoutHydrating(
2721 + current,
2722 + workInProgress,
2723 + nextPrimaryChildren,
2724 + nextFallbackChildren,
2725 + renderLanes,
2726 + );
2727 + const primaryChildFragment: Fiber = (workInProgress.child: any);
2728 + primaryChildFragment.memoizedState = mountSuspenseOffscreenState(
2729 + renderLanes,
2730 + );
2731 + workInProgress.memoizedState = SUSPENDED_MARKER;
2732 + return fallbackChildFragment;
2733 + }
2734 }
2735 }
2736
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+114 -76
@@ -759,6 +759,93 @@ function bubbleProperties(completedWork: Fiber) {
759 return didBailout;
760 }
761
762 +function completeDehydratedSuspenseBoundary(
763 + current: Fiber | null,
764 + workInProgress: Fiber,
765 + nextState: SuspenseState | null,
766 +): boolean {
767 + if (
768 + hasUnhydratedTailNodes() &&
769 + (workInProgress.mode & ConcurrentMode) !== NoMode &&
770 + (workInProgress.flags & DidCapture) === NoFlags
771 + ) {
772 + warnIfUnhydratedTailNodes(workInProgress);
773 + resetHydrationState();
774 + workInProgress.flags |= ForceClientRender | Incomplete | ShouldCapture;
775 +
776 + return false;
777 + }
778 +
779 + const wasHydrated = popHydrationState(workInProgress);
780 +
781 + if (nextState !== null && nextState.dehydrated !== null) {
782 + // We might be inside a hydration state the first time we're picking up this
783 + // Suspense boundary, and also after we've reentered it for further hydration.
784 + if (current === null) {
785 + if (!wasHydrated) {
786 + throw new Error(
787 + 'A dehydrated suspense component was completed without a hydrated node. ' +
788 + 'This is probably a bug in React.',
789 + );
790 + }
791 + prepareToHydrateHostSuspenseInstance(workInProgress);
792 + bubbleProperties(workInProgress);
793 + if (enableProfilerTimer) {
794 + if ((workInProgress.mode & ProfileMode) !== NoMode) {
795 + const isTimedOutSuspense = nextState !== null;
796 + if (isTimedOutSuspense) {
797 + // Don't count time spent in a timed out Suspense subtree as part of the base duration.
798 + const primaryChildFragment = workInProgress.child;
799 + if (primaryChildFragment !== null) {
800 + // $FlowFixMe Flow doesn't support type casting in combination with the -= operator
801 + workInProgress.treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
802 + }
803 + }
804 + }
805 + }
806 + return false;
807 + } else {
808 + // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
809 + // state since we're now exiting out of it. popHydrationState doesn't do that for us.
810 + resetHydrationState();
811 + if ((workInProgress.flags & DidCapture) === NoFlags) {
812 + // This boundary did not suspend so it's now hydrated and unsuspended.
813 + workInProgress.memoizedState = null;
814 + }
815 + // If nothing suspended, we need to schedule an effect to mark this boundary
816 + // as having hydrated so events know that they're free to be invoked.
817 + // It's also a signal to replay events and the suspense callback.
818 + // If something suspended, schedule an effect to attach retry listeners.
819 + // So we might as well always mark this.
820 + workInProgress.flags |= Update;
821 + bubbleProperties(workInProgress);
822 + if (enableProfilerTimer) {
823 + if ((workInProgress.mode & ProfileMode) !== NoMode) {
824 + const isTimedOutSuspense = nextState !== null;
825 + if (isTimedOutSuspense) {
826 + // Don't count time spent in a timed out Suspense subtree as part of the base duration.
827 + const primaryChildFragment = workInProgress.child;
828 + if (primaryChildFragment !== null) {
829 + // $FlowFixMe Flow doesn't support type casting in combination with the -= operator
830 + workInProgress.treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
831 + }
832 + }
833 + }
834 + }
835 + return false;
836 + }
837 + } else {
838 + // Successfully completed this tree. If this was a forced client render,
839 + // there may have been recoverable errors during first hydration
840 + // attempt. If so, add them to a queue so we can log them in the
841 + // commit phase.
842 + upgradeHydrationErrorsToRecoverable();
843 +
844 + // Fall through to normal Suspense path
845 + return true;
846 + }
847 +}
848 +
849 function completeWork(
850 current: Fiber | null,
851 workInProgress: Fiber,
@@ -996,80 +1083,35 @@ function completeWork(
1083 popSuspenseContext(workInProgress);
1084 const nextState: null | SuspenseState = workInProgress.memoizedState;
1085
1086 + // Special path for dehydrated boundaries. We may eventually move this
1087 + // to its own fiber type so that we can add other kinds of hydration
1088 + // boundaries that aren't associated with a Suspense tree. In anticipation
1089 + // of such a refactor, all the hydration logic is contained in
1090 + // this branch.
1091 if (
1000 - hasUnhydratedTailNodes() &&
1001 - (workInProgress.mode & ConcurrentMode) !== NoMode &&
1002 - (workInProgress.flags & DidCapture) === NoFlags
1092 + current === null ||
1093 + (current.memoizedState !== null &&
1094 + current.memoizedState.dehydrated !== null)
1095 ) {
1004 - warnIfUnhydratedTailNodes(workInProgress);
1005 - resetHydrationState();
1006 - workInProgress.flags |= ForceClientRender | Incomplete | ShouldCapture;
1007 - return workInProgress;
1008 - }
1009 - if (nextState !== null && nextState.dehydrated !== null) {
1010 - // We might be inside a hydration state the first time we're picking up this
1011 - // Suspense boundary, and also after we've reentered it for further hydration.
1012 - const wasHydrated = popHydrationState(workInProgress);
1013 - if (current === null) {
1014 - if (!wasHydrated) {
1015 - throw new Error(
1016 - 'A dehydrated suspense component was completed without a hydrated node. ' +
1017 - 'This is probably a bug in React.',
1018 - );
1019 - }
1020 - prepareToHydrateHostSuspenseInstance(workInProgress);
1021 - bubbleProperties(workInProgress);
1022 - if (enableProfilerTimer) {
1023 - if ((workInProgress.mode & ProfileMode) !== NoMode) {
1024 - const isTimedOutSuspense = nextState !== null;
1025 - if (isTimedOutSuspense) {
1026 - // Don't count time spent in a timed out Suspense subtree as part of the base duration.
1027 - const primaryChildFragment = workInProgress.child;
1028 - if (primaryChildFragment !== null) {
1029 - // $FlowFixMe Flow doesn't support type casting in combination with the -= operator
1030 - workInProgress.treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
1031 - }
1032 - }
1033 - }
1034 - }
1035 - return null;
1036 - } else {
1037 - // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
1038 - // state since we're now exiting out of it. popHydrationState doesn't do that for us.
1039 - resetHydrationState();
1040 - if ((workInProgress.flags & DidCapture) === NoFlags) {
1041 - // This boundary did not suspend so it's now hydrated and unsuspended.
1042 - workInProgress.memoizedState = null;
1043 - }
1044 - // If nothing suspended, we need to schedule an effect to mark this boundary
1045 - // as having hydrated so events know that they're free to be invoked.
1046 - // It's also a signal to replay events and the suspense callback.
1047 - // If something suspended, schedule an effect to attach retry listeners.
1048 - // So we might as well always mark this.
1049 - workInProgress.flags |= Update;
1050 - bubbleProperties(workInProgress);
1051 - if (enableProfilerTimer) {
1052 - if ((workInProgress.mode & ProfileMode) !== NoMode) {
1053 - const isTimedOutSuspense = nextState !== null;
1054 - if (isTimedOutSuspense) {
1055 - // Don't count time spent in a timed out Suspense subtree as part of the base duration.
1056 - const primaryChildFragment = workInProgress.child;
1057 - if (primaryChildFragment !== null) {
1058 - // $FlowFixMe Flow doesn't support type casting in combination with the -= operator
1059 - workInProgress.treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
1060 - }
1061 - }
1062 - }
1096 + const fallthroughToNormalSuspensePath = completeDehydratedSuspenseBoundary(
1097 + current,
1098 + workInProgress,
1099 + nextState,
1100 + );
1101 + if (!fallthroughToNormalSuspensePath) {
1102 + if (workInProgress.flags & ShouldCapture) {
1103 + // Special case. There were remaining unhydrated nodes. We treat
1104 + // this as a mismatch. Revert to client rendering.
1105 + return workInProgress;
1106 + } else {
1107 + // Did not finish hydrating, either because this is the initial
1108 + // render or because something suspended.
1109 + return null;
1110 }
1064 - return null;
1111 }
1066 - }
1112
1068 - // Successfully completed this tree. If this was a forced client render,
1069 - // there may have been recoverable errors during first hydration
1070 - // attempt. If so, add them to a queue so we can log them in the
1071 - // commit phase.
1072 - upgradeHydrationErrorsToRecoverable();
1113 + // Continue with the normal Suspense path.
1114 + }
1115
1116 if ((workInProgress.flags & DidCapture) !== NoFlags) {
1117 // Something suspended. Re-render with the fallback children.
@@ -1086,13 +1128,9 @@ function completeWork(
1128 }
1129
1130 const nextDidTimeout = nextState !== null;
1089 - let prevDidTimeout = false;
1090 - if (current === null) {
1091 - popHydrationState(workInProgress);
1092 - } else {
1093 - const prevState: null | SuspenseState = current.memoizedState;
1094 - prevDidTimeout = prevState !== null;
1095 - }
1131 + const prevDidTimeout =
1132 + current !== null &&
1133 + (current.memoizedState: null | SuspenseState) !== null;
1134
1135 if (enableCache && nextDidTimeout) {
1136 const offscreenFiber: Fiber = (workInProgress.child: any);
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+114 -76
@@ -759,6 +759,93 @@ function bubbleProperties(completedWork: Fiber) {
759 return didBailout;
760 }
761
762 +function completeDehydratedSuspenseBoundary(
763 + current: Fiber | null,
764 + workInProgress: Fiber,
765 + nextState: SuspenseState | null,
766 +): boolean {
767 + if (
768 + hasUnhydratedTailNodes() &&
769 + (workInProgress.mode & ConcurrentMode) !== NoMode &&
770 + (workInProgress.flags & DidCapture) === NoFlags
771 + ) {
772 + warnIfUnhydratedTailNodes(workInProgress);
773 + resetHydrationState();
774 + workInProgress.flags |= ForceClientRender | Incomplete | ShouldCapture;
775 +
776 + return false;
777 + }
778 +
779 + const wasHydrated = popHydrationState(workInProgress);
780 +
781 + if (nextState !== null && nextState.dehydrated !== null) {
782 + // We might be inside a hydration state the first time we're picking up this
783 + // Suspense boundary, and also after we've reentered it for further hydration.
784 + if (current === null) {
785 + if (!wasHydrated) {
786 + throw new Error(
787 + 'A dehydrated suspense component was completed without a hydrated node. ' +
788 + 'This is probably a bug in React.',
789 + );
790 + }
791 + prepareToHydrateHostSuspenseInstance(workInProgress);
792 + bubbleProperties(workInProgress);
793 + if (enableProfilerTimer) {
794 + if ((workInProgress.mode & ProfileMode) !== NoMode) {
795 + const isTimedOutSuspense = nextState !== null;
796 + if (isTimedOutSuspense) {
797 + // Don't count time spent in a timed out Suspense subtree as part of the base duration.
798 + const primaryChildFragment = workInProgress.child;
799 + if (primaryChildFragment !== null) {
800 + // $FlowFixMe Flow doesn't support type casting in combination with the -= operator
801 + workInProgress.treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
802 + }
803 + }
804 + }
805 + }
806 + return false;
807 + } else {
808 + // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
809 + // state since we're now exiting out of it. popHydrationState doesn't do that for us.
810 + resetHydrationState();
811 + if ((workInProgress.flags & DidCapture) === NoFlags) {
812 + // This boundary did not suspend so it's now hydrated and unsuspended.
813 + workInProgress.memoizedState = null;
814 + }
815 + // If nothing suspended, we need to schedule an effect to mark this boundary
816 + // as having hydrated so events know that they're free to be invoked.
817 + // It's also a signal to replay events and the suspense callback.
818 + // If something suspended, schedule an effect to attach retry listeners.
819 + // So we might as well always mark this.
820 + workInProgress.flags |= Update;
821 + bubbleProperties(workInProgress);
822 + if (enableProfilerTimer) {
823 + if ((workInProgress.mode & ProfileMode) !== NoMode) {
824 + const isTimedOutSuspense = nextState !== null;
825 + if (isTimedOutSuspense) {
826 + // Don't count time spent in a timed out Suspense subtree as part of the base duration.
827 + const primaryChildFragment = workInProgress.child;
828 + if (primaryChildFragment !== null) {
829 + // $FlowFixMe Flow doesn't support type casting in combination with the -= operator
830 + workInProgress.treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
831 + }
832 + }
833 + }
834 + }
835 + return false;
836 + }
837 + } else {
838 + // Successfully completed this tree. If this was a forced client render,
839 + // there may have been recoverable errors during first hydration
840 + // attempt. If so, add them to a queue so we can log them in the
841 + // commit phase.
842 + upgradeHydrationErrorsToRecoverable();
843 +
844 + // Fall through to normal Suspense path
845 + return true;
846 + }
847 +}
848 +
849 function completeWork(
850 current: Fiber | null,
851 workInProgress: Fiber,
@@ -996,80 +1083,35 @@ function completeWork(
1083 popSuspenseContext(workInProgress);
1084 const nextState: null | SuspenseState = workInProgress.memoizedState;
1085
1086 + // Special path for dehydrated boundaries. We may eventually move this
1087 + // to its own fiber type so that we can add other kinds of hydration
1088 + // boundaries that aren't associated with a Suspense tree. In anticipation
1089 + // of such a refactor, all the hydration logic is contained in
1090 + // this branch.
1091 if (
1000 - hasUnhydratedTailNodes() &&
1001 - (workInProgress.mode & ConcurrentMode) !== NoMode &&
1002 - (workInProgress.flags & DidCapture) === NoFlags
1092 + current === null ||
1093 + (current.memoizedState !== null &&
1094 + current.memoizedState.dehydrated !== null)
1095 ) {
1004 - warnIfUnhydratedTailNodes(workInProgress);
1005 - resetHydrationState();
1006 - workInProgress.flags |= ForceClientRender | Incomplete | ShouldCapture;
1007 - return workInProgress;
1008 - }
1009 - if (nextState !== null && nextState.dehydrated !== null) {
1010 - // We might be inside a hydration state the first time we're picking up this
1011 - // Suspense boundary, and also after we've reentered it for further hydration.
1012 - const wasHydrated = popHydrationState(workInProgress);
1013 - if (current === null) {
1014 - if (!wasHydrated) {
1015 - throw new Error(
1016 - 'A dehydrated suspense component was completed without a hydrated node. ' +
1017 - 'This is probably a bug in React.',
1018 - );
1019 - }
1020 - prepareToHydrateHostSuspenseInstance(workInProgress);
1021 - bubbleProperties(workInProgress);
1022 - if (enableProfilerTimer) {
1023 - if ((workInProgress.mode & ProfileMode) !== NoMode) {
1024 - const isTimedOutSuspense = nextState !== null;
1025 - if (isTimedOutSuspense) {
1026 - // Don't count time spent in a timed out Suspense subtree as part of the base duration.
1027 - const primaryChildFragment = workInProgress.child;
1028 - if (primaryChildFragment !== null) {
1029 - // $FlowFixMe Flow doesn't support type casting in combination with the -= operator
1030 - workInProgress.treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
1031 - }
1032 - }
1033 - }
1034 - }
1035 - return null;
1036 - } else {
1037 - // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration
1038 - // state since we're now exiting out of it. popHydrationState doesn't do that for us.
1039 - resetHydrationState();
1040 - if ((workInProgress.flags & DidCapture) === NoFlags) {
1041 - // This boundary did not suspend so it's now hydrated and unsuspended.
1042 - workInProgress.memoizedState = null;
1043 - }
1044 - // If nothing suspended, we need to schedule an effect to mark this boundary
1045 - // as having hydrated so events know that they're free to be invoked.
1046 - // It's also a signal to replay events and the suspense callback.
1047 - // If something suspended, schedule an effect to attach retry listeners.
1048 - // So we might as well always mark this.
1049 - workInProgress.flags |= Update;
1050 - bubbleProperties(workInProgress);
1051 - if (enableProfilerTimer) {
1052 - if ((workInProgress.mode & ProfileMode) !== NoMode) {
1053 - const isTimedOutSuspense = nextState !== null;
1054 - if (isTimedOutSuspense) {
1055 - // Don't count time spent in a timed out Suspense subtree as part of the base duration.
1056 - const primaryChildFragment = workInProgress.child;
1057 - if (primaryChildFragment !== null) {
1058 - // $FlowFixMe Flow doesn't support type casting in combination with the -= operator
1059 - workInProgress.treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
1060 - }
1061 - }
1062 - }
1096 + const fallthroughToNormalSuspensePath = completeDehydratedSuspenseBoundary(
1097 + current,
1098 + workInProgress,
1099 + nextState,
1100 + );
1101 + if (!fallthroughToNormalSuspensePath) {
1102 + if (workInProgress.flags & ShouldCapture) {
1103 + // Special case. There were remaining unhydrated nodes. We treat
1104 + // this as a mismatch. Revert to client rendering.
1105 + return workInProgress;
1106 + } else {
1107 + // Did not finish hydrating, either because this is the initial
1108 + // render or because something suspended.
1109 + return null;
1110 }
1064 - return null;
1111 }
1066 - }
1112
1068 - // Successfully completed this tree. If this was a forced client render,
1069 - // there may have been recoverable errors during first hydration
1070 - // attempt. If so, add them to a queue so we can log them in the
1071 - // commit phase.
1072 - upgradeHydrationErrorsToRecoverable();
1113 + // Continue with the normal Suspense path.
1114 + }
1115
1116 if ((workInProgress.flags & DidCapture) !== NoFlags) {
1117 // Something suspended. Re-render with the fallback children.
@@ -1086,13 +1128,9 @@ function completeWork(
1128 }
1129
1130 const nextDidTimeout = nextState !== null;
1089 - let prevDidTimeout = false;
1090 - if (current === null) {
1091 - popHydrationState(workInProgress);
1092 - } else {
1093 - const prevState: null | SuspenseState = current.memoizedState;
1094 - prevDidTimeout = prevState !== null;
1095 - }
1131 + const prevDidTimeout =
1132 + current !== null &&
1133 + (current.memoizedState: null | SuspenseState) !== null;
1134
1135 if (enableCache && nextDidTimeout) {
1136 const offscreenFiber: Fiber = (workInProgress.child: any);