router icon indicating copy to clipboard operation
router copied to clipboard

Router with basepath responds to all history events, breaking MFE architectures

Open Diveafall opened this issue 1 month ago โ€ข 2 comments

Description

When multiple TanStack Routers coexist (e.g., in micro-frontend architectures), all routers respond to all history changes, even when the path is outside their configured basepath. This causes:

  • 404 errors from the wrong router
  • defaultNotFoundComponent triggering incorrectly
  • Broken navigation between shell and MFE modules

Reproduction

Repository: https://github.com/Diveafall/tanstack-router-mfe-basepath-bug

git clone https://github.com/Diveafall/tanstack-router-mfe-basepath-bug
cd tanstack-router-mfe-basepath-bug
npm install
npm run dev

Steps to Reproduce

  1. Open http://localhost:5173
  2. Click "MFE Page 1" to load the MFE (it works correctly)
  3. Click "Settings" or "Home" in the shell navigation
  4. BUG: The MFE container (which stays mounted) shows "404 - Not Found in MFE"

Important: In this reproduction, the MFE stays mounted after first load to simulate real MFE architectures where:

  • The MFE is loaded once and stays in the DOM (persistent container)
  • The shell controls the URL via hash history
  • Both routers subscribe to the same hash changes

Architecture

Shell Application (TanStack Router, hash history)
โ”œโ”€โ”€ / (home)
โ”œโ”€โ”€ /settings
โ””โ”€โ”€ /mfe/* (splat route โ†’ loads MFE web component)

MFE Application (TanStack Router, hash history, basepath: '/mfe')
โ”œโ”€โ”€ /mfe/page1
โ””โ”€โ”€ /mfe/page2

Expected Behavior

When navigating to /settings (outside the MFE's basepath /mfe):

  • MFE router should ignore the history event (path is out of scope)
  • MFE should continue showing its last valid state, or show nothing

Actual Behavior

When navigating to /settings:

  • MFE router processes the history event
  • MFE router tries to match /settings against its routes
  • No match โ†’ MFE's defaultNotFoundComponent renders "404 - Not Found"

Root Cause Analysis

In @tanstack/react-router, the Transitioner.tsx component subscribes to history changes:

// packages/react-router/src/Transitioner.tsx:44
router.history.subscribe(router.load)

This subscription does not filter by basepath. Every router receives every history event, regardless of whether the path is within its basepath scope.

The basepath option is currently only used for:

  1. Prefixing generated links
  2. Stripping the prefix when matching routes

But it does NOT filter which history events the router processes.

How React Router Handles This

React Router uses a stripBasename function that returns null for paths outside the basename scope:

// @remix-run/router
export function stripBasename(pathname: string, basename: string): string | null {
  if (basename === "/") return pathname;
  if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
    return null;  // Path outside basename scope - IGNORE
  }
  let nextChar = pathname.charAt(basename.length);
  if (nextChar && nextChar !== "/") {
    return null;  // Must have / after basename (prevents /app matching /application)
  }
  return pathname.slice(basename.length) || "/";
}

When stripBasename() returns null:

  • matchRoutes() returns null
  • Nothing renders - the router completely ignores out-of-scope paths
  • No 404, no redirect, just silent ignore

Proposed Fix

I've created a PR with a fix: #6063

The fix adds a basepath scope check at the beginning of the router's load method:

// router.ts
load = async (opts) => {
  // If this router has a basepath, only respond to paths within scope
  if (this.basepath && this.basepath !== '/') {
    let pathToCheck = this.history.location.pathname;
    
    // For hash history, extract path from the hash portion
    const href = this.history.location.href;
    if (href.includes('#')) {
      const hashPart = href.split('#')[1];
      if (hashPart) {
        pathToCheck = hashPart.split('?')[0]?.split('#')[0] || '/';
      }
    }
    
    if (!isPathInScope(pathToCheck, this.basepath)) {
      return; // Silent ignore - let other routers handle it
    }
  }
  // ... rest of load logic
}

This mirrors React Router's behavior - if you set basepath: '/app', the router only cares about paths like /app/*.

Environment

  • @tanstack/react-router: 1.120.3
  • Browser: Chrome/Firefox/Safari (all affected)
  • History type: Hash history (but browser history has the same issue)

Related Discussions

  • #2103 - Multiple routers in different packages
  • #2108 - MFE "not found" issue

Diveafall avatar Dec 10 '25 23:12 Diveafall

๐Ÿ“ CodeRabbit Plan Mode

Generate an implementation plan and prompts that you can use with your favorite coding agent.

  • [ ] Create Plan
Examples

๐Ÿ”— Related PRs

TanStack/router#5242 - fix(router): hash history links with target="_blank" generate incorrect URLs [merged] TanStack/router#5244 - fix(router): rewriteBasepath not working without trailing slash [merged] TanStack/router#5330 - fix: correctly handle client-side vs server-side redirects with rewrites [merged] TanStack/router#5710 - fix: route HMR handling [merged] TanStack/router#5944 - fix: Links always scroll to URL hash on hover (#5930) [merged]

๐Ÿ‘ค Suggested Assignees

  • schiller-manuel
  • leesb971204
  • thenglong
  • Shreyas-Sarkar

๐Ÿงช Issue enrichment is currently in early access.

To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

issue_enrichment:
  auto_enrich:
    enabled: false

coderabbitai[bot] avatar Dec 10 '25 23:12 coderabbitai[bot]

Reproduction Verified โœ…

I've verified that the reproduction repository correctly demonstrates the bug. Just pushed a fix to ensure the MFE stays mounted when navigating to shell routes.

Test Results

Steps performed:

  1. Navigate to home page (#/)
  2. Click "MFE Page 1" โ†’ MFE loads correctly at #/mfe/page1
  3. Click "Settings" โ†’ Navigate to #/settings

Result: MFE (which stays mounted) shows "404 - Not Found in MFE" even though:

  • The URL is #/settings (outside MFE's basepath /mfe)
  • The Shell's Settings page renders correctly
  • The MFE should ignore paths outside its basepath scope

Key Observation

The bug clearly shows that a router with basepath: '/mfe' is incorrectly trying to match paths like /settings instead of ignoring them. This is the core issue that PR #6063 addresses.

The fix in #6063 adds basepath scope checking at the router level, allowing MFE routers to silently ignore history events outside their scope - matching React Router's behavior.

Diveafall avatar Dec 10 '25 23:12 Diveafall

We are experiencing the same issue in our MFE set-up

nicwells avatar Dec 12 '25 14:12 nicwells