WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Conversation

@Diveafall
Copy link

@Diveafall Diveafall commented Dec 10, 2025

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: defaultNotFoundComponent triggers 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:

  1. Prefixing generated links
  2. 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

  1. New utility function isPathInScope(pathname, basepath) in path.ts:

    • Returns true if the pathname is within the basepath scope
    • Case-insensitive comparison (matches React Router behavior)
    • Ensures basepath boundary check (e.g., /app doesn't match /application)
  2. Basepath check in router.load():

    • If basepath is 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

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

  1. User is on /user-management/users (MFE is loaded)
  2. User clicks link to /settings (shell route)
  3. ❌ MFE router receives history event
  4. ❌ MFE router tries to match /settings
  5. ❌ No match → 404 or defaultNotFoundComponent renders

After This Fix

  1. User is on /user-management/users (MFE is loaded)
  2. User clicks link to /settings (shell route)
  3. ✅ MFE router receives history event
  4. ✅ MFE router checks: is /settings within /user-management? No
  5. ✅ MFE router returns early (silent ignore)
  6. ✅ Shell router handles /settings correctly

Breaking Changes

None for most users.

  • Routers without basepath (or basepath: '/') behave exactly the same
  • Routers with basepath will 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 (/app vs /application)
    • Case-insensitive comparison
    • Trailing slashes
    • Edge cases

All existing tests pass (213 path tests total).

Files Changed

  • packages/router-core/src/path.ts - Added isPathInScope() utility
  • packages/router-core/src/router.ts - Added basepath scope check in load()
  • packages/router-core/tests/path.test.ts - Added unit tests

Fixes #2103
Fixes #2108

Summary by CodeRabbit

  • New Features
    • Implemented basepath scope validation. The router now prevents navigation to paths outside the configured basepath scope, silently ignoring out-of-scope navigation requests. This enhancement applies to both standard URL routing and hash-based routing configurations.

✏️ Tip: You can customize this high-level summary in your review settings.

When a router has a basepath configured, it now only processes location
changes that are within its basepath scope. This enables micro-frontend
architectures where multiple TanStack routers coexist.

Changes:
- Add isPathInScope() utility to check if a path is within basepath scope
- Add basepath check at start of router.load() method
- Add comprehensive unit tests for isPathInScope()

The implementation mirrors React Router's stripBasename behavior where
paths outside the basename scope are silently ignored.

Fixes TanStack#2103
Fixes TanStack#2108

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 10, 2025

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

  • isPathInScope boundary logic: Verify case-insensitive prefix matching correctly handles '/' boundaries to prevent partial matches (e.g., /app should not match /application)
  • Hash routing path extraction: Confirm pathToCheck computation correctly extracts the evaluable path from both history.location.pathname and 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

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 82e8044 and 9a3d845.

📒 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 /app matching /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 isPathInScope utility across multiple dimensions:

  • Root basepath universality (lines 90-94)
  • Exact and prefix matching (lines 96-107)
  • Boundary validation preventing false positives like /app vs /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 in packages/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's load() 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

}

load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
// If this router has a basepath, only respond to paths within scope.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we add support for opting out of navigation, we need to check where that should happen best.
if that's here, then i would rather add an optional callback function that can execute arbitrary user code instead of adding this logic here.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants