fix: scope router to basepath for MFE support
Problem
When multiple TanStack routers coexist in a micro-frontend (MFE) architecture, all routers respond to all history changes, even when the path is outside their configured basepath. This causes unexpected behavior:
- 404 errors: MFE router tries to match paths it shouldn't handle
-
Navigation conflicts:
defaultNotFoundComponenttriggers incorrectly - Broken user experience: Users can't navigate between shell and MFE modules
Root Cause
In Transitioner.tsx (line 44), every router subscribes to history changes via:
router.history.subscribe(router.load)
This means when the shell navigates to /settings, an MFE router with basepath: '/user-management' still receives the event and attempts to match /settings against its routes - which fails, triggering a 404.
Why This Happens
TanStack Router's 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. A router with basepath: '/app' will still try to process navigation to /, /settings, /other-module, etc.
Solution
Add a basepath scope check at the beginning of the router's load method. When a router has a basepath configured, it now only processes location changes within its basepath scope.
Implementation
-
New utility function
isPathInScope(pathname, basepath)inpath.ts:- Returns
trueif the pathname is within the basepath scope - Case-insensitive comparison (matches React Router behavior)
- Ensures basepath boundary check (e.g.,
/appdoesn't match/application)
- Returns
-
Basepath check in
router.load():- If
basepathis set and not/, check if current path is in scope - If out of scope, return early (silent ignore - no 404, no redirect)
- Supports both browser history and hash history
- If
How React Router Handles This
This implementation mirrors React Router's stripBasename behavior:
// React Router's approach (from @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
}
return pathname.slice(basename.length) || "/";
}
When stripBasename() returns null, matchRoutes() returns null, and nothing renders - the router completely ignores out-of-scope paths.
Use Case: Micro-Frontends
Shell Application (TanStack Router)
βββ / (home)
βββ /settings
βββ /user-management/* (splat route loads MFE)
MFE Application (TanStack Router, basepath: '/user-management')
βββ /user-management/users
βββ /user-management/roles
βββ /user-management/permissions
Before This Fix
- User is on
/user-management/users(MFE is loaded) - User clicks link to
/settings(shell route) - β MFE router receives history event
- β MFE router tries to match
/settings - β No match β 404 or
defaultNotFoundComponentrenders
After This Fix
- User is on
/user-management/users(MFE is loaded) - User clicks link to
/settings(shell route) - β MFE router receives history event
- β
MFE router checks: is
/settingswithin/user-management? No - β MFE router returns early (silent ignore)
- β
Shell router handles
/settingscorrectly
Breaking Changes
None for most users.
- Routers without
basepath(orbasepath: '/') behave exactly the same - Routers with
basepathwill now correctly ignore out-of-scope paths
This is actually fixing the expected behavior. If you set basepath: '/app', you intuitively expect the router to only care about paths like /app/*.
Testing
- Added 8 comprehensive unit tests for
isPathInScope():- Root basepath always returns true
- Exact basepath match
- Pathname starting with basepath followed by
/ - Pathname not starting with basepath
- Basepath as prefix but not at path boundary (
/appvs/application) - Case-insensitive comparison
- Trailing slashes
- Edge cases
All existing tests pass (213 path tests total).
Files Changed
-
packages/router-core/src/path.ts- AddedisPathInScope()utility -
packages/router-core/src/router.ts- Added basepath scope check inload() -
packages/router-core/tests/path.test.ts- Added unit tests
Fixes #2103 Fixes #2108
Walkthrough
Added a new isPathInScope utility function to determine if a pathname resides within a configured basepath, with case-insensitive prefix matching and strict boundary validation. Integrated this function into RouterCore.load to silently ignore navigation outside the defined scope, including hash-based routing. Comprehensive tests validate the utility across multiple basepath configurations.
Changes
| Cohort / File(s) | Summary |
|---|---|
Path utility function packages/router-core/src/path.ts |
Added new exported function isPathInScope(pathname: string, basepath: string): boolean that performs case-insensitive prefix matching with strict boundary checks to prevent partial path matches. |
Router scope guard packages/router-core/src/router.ts |
Integrated basepath validation in RouterCore.load method; imported and applied isPathInScope to silently ignore out-of-scope navigation, handling both standard and hash-based routing scenarios. |
Test coverage packages/router-core/tests/path.test.ts |
Added comprehensive tests for isPathInScope covering root path handling, exact matches, prefix-with-slash validation, case-insensitive comparisons, trailing slash behavior, and boundary edge cases. |
Estimated code review effort
π― 3 (Moderate) | β±οΈ ~20 minutes
-
isPathInScopeboundary logic: Verify case-insensitive prefix matching correctly handles '/' boundaries to prevent partial matches (e.g.,/appshould not match/application) -
Hash routing path extraction: Confirm
pathToCheckcomputation correctly extracts the evaluable path from bothhistory.location.pathnameand hash fragments - Early return behavior: Validate that silently returning when out-of-scope matches intended React Router basepath behavior without breaking existing navigation flows
Possibly related PRs
-
TanStack/router#5169: Modifies pathname handling in
packages/router-core/src/path.tswith related changes tobaseParsePathnamedecoding logic.
Suggested reviewers
- schiller-manuel
- Sheraff
Poem
π° A basepath scope guard hops into place, Checking if paths are in bounds with grace, No more wandering beyond the fence, Hash or slashβthe logic makes sense! π
Pre-merge checks and finishing touches
β Passed checks (3 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | β Passed | Check skipped - CodeRabbitβs high-level summary is enabled. |
| Title check | β Passed | The title accurately describes the main change: adding basepath scoping to the router to support multi-frontend (MFE) setups, which is the core objective of the PR. |
| Docstring Coverage | β Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
β¨ Finishing touches
- [ ] π Generate docstrings
π§ͺ Generate unit tests (beta)
- [ ] Create PR with unit tests
- [ ] Post copyable unit tests in a comment
π Recent review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π₯ Commits
Reviewing files that changed from the base of the PR and between 82e80446fe66518b60593a010f396a4c8047c6e5 and 9a3d845b759512bbbeabdadfe8c3bdbaf28b87b2.
π Files selected for processing (3)
-
packages/router-core/src/path.ts(1 hunks) -
packages/router-core/src/router.ts(2 hunks) -
packages/router-core/tests/path.test.ts(2 hunks)
π§° Additional context used
π Path-based instructions (2)
**/*.{ts,tsx}
π CodeRabbit inference engine (AGENTS.md)
Use TypeScript strict mode with extensive type safety for all code
Files:
-
packages/router-core/src/path.ts -
packages/router-core/tests/path.test.ts -
packages/router-core/src/router.ts
**/*.{js,ts,tsx}
π CodeRabbit inference engine (AGENTS.md)
Implement ESLint rules for router best practices using the ESLint plugin router
Files:
-
packages/router-core/src/path.ts -
packages/router-core/tests/path.test.ts -
packages/router-core/src/router.ts
π§ Learnings (5)
π Common learnings
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/src/routes/non-nested/named/$baz_.bar.tsx:3-5
Timestamp: 2025-09-22T00:56:49.237Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments (e.g., `$baz_` becomes `baz` in generated types) but should be preserved in base path segments. This is the correct behavior as of the fix in PR #5182.
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` β `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts:167-172
Timestamp: 2025-09-22T00:56:53.426Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments during path parsing, but preserved in base path segments. This is the expected behavior implemented in PR #5182.
π Learning: 2025-10-01T18:30:26.591Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` β `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.
Applied to files:
-
packages/router-core/src/path.ts -
packages/router-core/src/router.ts
π Learning: 2025-10-08T08:11:47.088Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.
Applied to files:
-
packages/router-core/tests/path.test.ts -
packages/router-core/src/router.ts
π Learning: 2025-10-09T12:59:02.129Z
Learnt from: hokkyss
Repo: TanStack/router PR: 5418
File: e2e/react-start/custom-identifier-prefix/src/styles/app.css:19-21
Timestamp: 2025-10-09T12:59:02.129Z
Learning: In e2e test directories (paths containing `e2e/`), accessibility concerns like outline suppression patterns are less critical since the code is for testing purposes, not production use.
Applied to files:
-
packages/router-core/tests/path.test.ts
π Learning: 2025-12-06T15:03:07.223Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-06T15:03:07.223Z
Learning: Applies to **/*.{js,ts,tsx} : Implement ESLint rules for router best practices using the ESLint plugin router
Applied to files:
-
packages/router-core/src/router.ts
𧬠Code graph analysis (2)
packages/router-core/tests/path.test.ts (1)
packages/router-core/src/path.ts (1)
isPathInScope(367-379)
packages/router-core/src/router.ts (1)
packages/router-core/src/path.ts (1)
isPathInScope(367-379)
π Additional comments (4)
packages/router-core/src/path.ts (1)
355-379: Well-designed basepath scope utility for MFE support.The implementation correctly handles:
- Root basepath as universal scope (line 368)
- Case-insensitive matching to align with URL behavior (line 371)
- Strict boundary enforcement to prevent partial matches like
/appmatching/application(lines 377-378)This mirrors React Router's stripBasename behavior and is appropriate for the MFE use case described in the PR objectives.
packages/router-core/tests/path.test.ts (1)
89-141: Comprehensive test coverage for basepath scoping.The test suite thoroughly exercises the
isPathInScopeutility across multiple dimensions:
- Root basepath universality (lines 90-94)
- Exact and prefix matching (lines 96-107)
- Boundary validation preventing false positives like
/appvs/application(lines 116-121)- Case-insensitive behavior (lines 123-130)
- Trailing slash handling (lines 132-135)
- Edge cases with empty strings (lines 137-140)
This coverage aligns well with the MFE use case requirements.
packages/router-core/src/router.ts (2)
2101-2122: Well-integrated basepath scope guard for MFE support.The scope check is correctly positioned at the start of
load()before any side effects occur. Key design points:
- Silent early return for out-of-scope paths (line 2120) aligns with MFE requirements where multiple routers coexist
- Supports both browser history (line 2106) and hash history (lines 2108-2116) per PR objectives
- Root basepath
'/'treated as global (line 2104 condition)- No breaking changes for existing routers without basepath or with basepath
'/'This implementation successfully addresses the MFE navigation conflict issues described in PR #6063.
2108-2116: No special handling needed for hash fragments.Hash history routing is designed to use path-like fragments (e.g.,
#/app/page), as confirmed by test cases inpackages/history/tests/createHashHistory.test.ts. All test cases show fragments starting with/after the#symbol. Anchor-only hashes (e.g.,#section) are not routing destinationsβthey're in-page navigation and should not trigger the router'sload()method. The existing logic correctly handles the standard hash routing format, and the|| '/'fallback appropriately handles empty hashes. The basepath scope guard works as intended for all valid hash routing scenarios.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.