Skip to content

React: a no-SSR route logs a hydration mismatch with pendingMinMs: 0 since #8643 #8647

Closed

Description

@Tarekkharsa

Which project does this relate to?

Router

Describe the bug

Since #8643, a ssr: 'data-only' or ssr: false route with a pending component logs a React hydration mismatch on a hard load when the client's pendingMinMs is 0. React then client-renders the boundary. This affects only main; the published 1.170.41 does not do it.

React hydrates the route's <Suspense> boundary after the shell. With a minimum of 0, the client load commits success before React reaches that boundary. MatchInner now decides from match.status alone, so it renders the route component where the server rendered the pending component:

<MatchInnerImpl match={{id:"/dashb...", ...}}>
  <Lazy>
    <Dashboard>
+     <h1 data-content={true}>
-     <div data-skeleton="true">

Before #8643, ClientOnly rendered its fallback from the server snapshot of useSyncExternalStore during hydration, whatever the match status, so this case could not mismatch. The #8640 regression tests use pendingMinMs: 200, which keeps the match pending until the boundary hydrates.

Complete minimal reproducer

https://github.com/Tarekkharsa/tanstack-router-pending-min-after-hydration-repro

The browser part needs main packed locally until a release contains #8643 (the README says how). The quickest check is the unit test below.

Steps to Reproduce the Bug

In packages/react-router, copy tests/issue-8640-data-only-pending-hydration.test.tsx, set the dashboard route's pendingMinMs to 0, and drop the toBeGreaterThanOrEqual(150) timing assertion, which only applies to a non-zero minimum.

Commit pendingMinMs: 200 pendingMinMs: 0
663282b (before #8643) 8 of 8 hydration cases fail (#8640) 9 of 9 pass
ff9f753 (#8643) pass 5 of 8 fail with a recoverable hydration error

The failing cases are data-only success, ssr: false success, data-only loader notFound, data-only unmatched URL, and ssr: false unmatched URL. The same change to the Solid and Vue copies of the test passes all cases.

In the browser reproduction, /dashboard?min=0 logs React error #418 on 6 of 6 loads on main, and 0 of 6 on 1.170.41.

Expected behavior

A no-SSR boundary hydrates against its server pending UI with any pendingMinMs, then shows the content.

Screenshots or Videos

No response

Platform

  • Router / Start Version: main at ff9f753; not in a release yet
  • OS: macOS
  • Browser: Chromium (Playwright 1.62.1), and jsdom in the unit test
  • Bundler: vite 8.3.1

Additional context

A change I tried in MatchInner (React only):

+  const hydrated = useHydrated(match.ssr === false || match.ssr === 'data-only')
+
   if (
     match.status === 'pending' ||
     ((isServer ?? router.isServer) &&
-      (match.ssr === false || match.ssr === 'data-only'))
+      (match.ssr === false || match.ssr === 'data-only')) ||
+    !hydrated
   ) {
     ...
-    if (router._tx) {
+    if (router._tx && match.status === 'pending') {
       throw router._tx[5]
     }
     return renderPending(router, route)

useHydrated(enabled) is the existing internal overload, so other matches keep the same snapshot on both sides and get no extra render. The second change is needed: during hydration the match can already be success while the transaction waits for that commit to render, and throwing router._tx[5] there makes them wait for each other.

With this change, the pendingMinMs: 0 copy and the existing #8640 test pass (18 of 18), the full @tanstack/react-router unit suite passes (99 files, 1248 tests), and the browser reproduction logs no #418. I have not run the type, lint, or bundle-size checks yet. I can open a PR with the pendingMinMs: 0 cases added to the #8640 test.

This also blocks #8648: a shorter hold after hydration produces the same commit-before-hydration timing.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions