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