Skip to content

bug: Hydration crash in CJS @tanstack/router-core due to fatal invariant in ssr-client.cjs (mismatch with ESM behaviour) #6806

@Double77x

Description

@Double77x

Which project does this relate to?

Router

Describe the bug

I am encountering a fatal Uncaught Error: Invariant failed crash during SSR hydration when my environment resolves the CommonJS (CJS) version of @tanstack/router-core (e.g., in certain Vite/SSR, Cloudflare Wrangler, or Node.js environments).

The crash occurs within the hydrate function in ssr-client.cjs. An aggressive invariant check assumes a child match must exist when isSpaMode is true. This is particularly problematic during hydration on 404/Error pages or when minor route ID mismatches occur, as it results in a "white screen of death" rather than a graceful recovery.

The Inconsistency

There is a direct logic mismatch between the ESM and CJS builds in @tanstack/router-core (v1.163.3):

1. The Crashing Code (CJS)

File: node_modules/@tanstack/router-core/dist/cjs/ssr/ssr-client.cjs

// Line ~189
if (isSpaMode) {
  const match = matches;
  // This throws a fatal error if match is undefined
  invariant(
    match,
    "Expected to find a match below the root match in SPA mode."
  );
  setMatchForcePending(match);
  // ...
}

2. The Safe Code (ESM)

File: node_modules/@tanstack/router-core/dist/esm/ssr/ssr-client.js

if (isSpaMode) {
  const match = matches;
  // The ESM version handles the missing match gracefully
  if (match) {
    setMatchForcePending(match);
    // ...
  } else {
    console.warn("SPA hydration: no match available for pending state");
  }
}

Impact

This discrepancy means the exact same application code may work perfectly in some environments (ESM) but crash in others (CJS) due to an inconsistent build artifact. In SSR contexts like Cloudflare Pages/Wrangler, the CJS entry point is often prioritised, leading to unavoidable crashes during hydration on non-existent routes.

Your Example Website or App

https://github.com/TanStack/router

Steps to Reproduce the Bug or Issue

Reproduction Steps

  1. Initialise a TanStack Start / Router project with SSR/Prerendering enabled.
  2. Force the environment to resolve CommonJS for @tanstack/router-core. This commonly occurs in production environments like Cloudflare Pages (Wrangler) or specific Node.js SSR setups where CJS is prioritized over ESM for core utilities.
  3. Trigger a hydration mismatch on a 404 route. For example: Navigate directly to a non-existent URL (e.g., /some-404-page). The server prerenders the page, but during hydration, the router calculates isSpaMode as true (often due to a minor ID mismatch or because it's a 404 transition).
  4. Observe the Hydration Logic: The hydrate function in node_modules/@tanstack/router-core/dist/cjs/ssr/ssr-client.cjs executes.
  5. The Crash: Because it is a 404/Error route, matches.length is 1 (Root only), meaning matches[1] is undefined.
  6. Fatal Error: The console will show: Uncaught Error: Invariant failed: Expected to find a match below the root match in SPA mode. This halts the JavaScript execution and prevents the application from becoming interactive.

Expected behavior

The CommonJS (CJS) build should be functionally identical to the ESM build. Specifically, the hydrate function should handle missing hydration data or child matches gracefully (via console.warn) rather than throwing fatal invariant errors that crash the entire application.

Proposed Fix for dist/cjs/ssr/ssr-client.cjs

The invariant should be replaced with a null-check, matching the existing logic found in the ESM version:

// Expected logic in dist/cjs/ssr/ssr-client.cjs
if (isSpaMode) {
  const match = matches;[1]

  // 1. Remove the fatal invariant:
  // invariant(match, "Expected to find a match below the root match in SPA mode.");

  // 2. Implement the safe check used in the ESM version:
  if (match) {
    setMatchForcePending(match);
    match._displayPending = true;
    match._nonReactive.displayPendingPromise = loadPromise;
    loadPromise.then(() => {
      batch.batch(() => {
        // ... (remaining pending logic)
      });
    });
  } else {
    // Graceful fallback for 404/Error routes during hydration
    console.warn("SPA hydration: no match available for pending state");
  }
}

By aligning these two builds, the router becomes resilient to minor hydration mismatches in CJS environments, preventing the "white screen of death" and allowing the application to remain interactive.

Screenshots or Videos

No response

Platform

  • Router / Start Version: 1.163.3
  • OS: Windows (win32)
  • Browser: Any modern browser (Chrome/Safari/Firefox/Edge)
  • Bundler: Vite
  • Bundler Version: 7.3.1
  • Deployment Platform: Cloudflare Pages / Wrangler (Relevant for CJS resolution)

Additional context

I've put an empty link into the reproduction URL field because this is a build-artifact inconsistency bug between the CJS and ESM distributions that is difficult to replicate in a browser-based StackBlitz (which defaults to ESM). I have provided direct UNPKG links to the conflicting files in the @tanstack/router-core package below:

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