Router with basepath responds to all history events, breaking MFE architectures
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15.1k
- Forks
- 1.9k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 143
Description
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
defaultNotFoundComponenttriggering 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
- Open
http://localhost:5173 - Click "MFE Page 1" to load the MFE (it works correctly)
- Click "Settings" or "Home" in the shell navigation
- 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
/settingsagainst its routes - No match → MFE's
defaultNotFoundComponentrenders "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:
- Prefixing generated links
- 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()returnsnull- 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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with packages/react-router/src/Transitioner.tsx and router.ts, then run the linked MFE reproduction to observe hash-history events from both routers. Trace router.load and verify that navigation outside basepath /mfe no longer renders the MFE's not-found state while /mfe/page1 and /mfe/page2 still work; PR #6063 already contains a proposed fix.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend, web-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100