Repository: facebook/react
Pull request: Move tail hydration mismatch back to hydration context

In #23176 we added a special case in completeWork for SuspenseBoundaries if they still have trailing children. However, that misses a case because it doesn't log a recoverable error for the hydration mismatch. So we get an error that we rerendered.

I think this special case was done to avoid contexts getting out of sync. I don't know why we didn't just move where the pop happens though so that's what I did here and let the regular pass throw instead. Seems to be pass the tests.


Changed files:
filename: packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
patch: @@ -538,6 +538,7 @@ describe('ReactDOMServerPartialHydration', () => {
     assertLog([
       'Server rendered',
       'Client rendered',
+      'Hydration failed because the initial UI does not match what was rendered on the server.',
       'There was an error while hydrating this Suspense boundary. ' +
         'Switched to client rendering.',
     ]);

filename: packages/react-reconciler/src/ReactFiberCompleteWork.js
patch: @@ -141,11 +141,9 @@ import {
   prepareToHydrateHostInstance,
   prepareToHydrateHostTextInstance,
   prepareToHydrateHostSuspenseInstance,
-  warnIfUnhydratedTailNodes,
   popHydrationState,
   resetHydrationState,
   getIsHydrating,
-  hasUnhydratedTailNodes,
   upgradeHydrationErrorsToRecoverable,
 } from './ReactFiberHydrationContext';
 import {
@@ -866,18 +864,6 @@ function completeDehydratedSuspenseBoundary(
   workInProgress: Fiber,
   nextState: SuspenseState | null,
 ): boolean {
-  if (
-    hasUnhydratedTailNodes() &&
-    (workInProgress.mode & ConcurrentMode) !== NoMode &&
-    (workInProgress.flags & DidCapture) === NoFlags
-  ) {
-    warnIfUnhydratedTailNodes(workInProgress);
-    resetHydrationState();
-    workInProgress.flags |= ForceClientRender | DidCapture;
-
-    return false;
-  }
-
   const wasHydrated = popHydrationState(workInProgress);
 
   if (nextState !== null && nextState.dehydrated !== null) {
@@ -1337,7 +1323,6 @@ function completeWork(
       return null;
     }
     case SuspenseComponent: {
-      popSuspenseHandler(workInProgress);
       const nextState: null | SuspenseState = workInProgress.memoizedState;
 
       // Special path for dehydrated boundaries. We may eventually move this
@@ -1358,10 +1343,12 @@ function completeWork(
           );
         if (!fallthroughToNormalSuspensePath) {
           if (workInProgress.flags & ForceClientRender) {
+            popSuspenseHandler(workInProgress);
             // Special case. There were remaining unhydrated nodes. We treat
             // this as a mismatch. Revert to client rendering.
             return workInProgress;
           } else {
+            popSuspenseHandler(workInProgress);
             // Did not finish hydrating, either because this is the initial
             // render or because something suspended.
             return null;
@@ -1371,6 +1358,8 @@ function completeWork(
         // Continue with the normal Suspense path.
       }
 
+      popSuspenseHandler(workInProgress);
+
       if ((workInProgress.flags & DidCapture) !== NoFlags) {
         // Something suspended. Re-render with the fallback children.
         workInProgress.lanes = renderLanes;

filename: packages/react-reconciler/src/ReactFiberHydrationContext.js
patch: @@ -893,10 +893,6 @@ function popHydrationState(fiber: Fiber): boolean {
   return true;
 }
 
-function hasUnhydratedTailNodes(): boolean {
-  return isHydrating && nextHydratableInstance !== null;
-}
-
 function warnIfUnhydratedTailNodes(fiber: Fiber) {
   let nextInstance = nextHydratableInstance;
   while (nextInstance) {
@@ -952,6 +948,4 @@ export {
   prepareToHydrateHostTextInstance,
   prepareToHydrateHostSuspenseInstance,
   popHydrationState,
-  hasUnhydratedTailNodes,
-  warnIfUnhydratedTailNodes,
 };