Skip to content

Conversation

CC-Bell
Copy link

@CC-Bell CC-Bell commented Sep 24, 2025

I am working on a application where we update the routeTree as new parts of the application is loaded (Module Federation architecture). When a new route is registered, these functions error out since the previous route no longer exists.

Summary by CodeRabbit

  • Bug Fixes
    • Improved router stability by safely handling missing routes during loader checks, preventing rare crashes when a route is undefined.
    • Updated devtools AgeTicker to gracefully handle absent routes, avoiding null-reference errors and returning no output when appropriate.
    • Overall, reduces runtime exceptions in edge cases involving unresolved or missing routes, enhancing reliability in both runtime and devtools views.

Copy link
Contributor

coderabbitai bot commented Sep 24, 2025

Walkthrough

Updates in router-core and devtools replace non-null assertions with optional access when resolving routes and checking loaders. Guard conditions now short-circuit when a route is missing or lacks a loader, affecting control flow by safely handling undefined routes during loader checks and AgeTicker rendering.

Changes

Cohort / File(s) Summary
Router core: safe route/loader checks
packages/router-core/src/router.ts
Removed forced non-null route access; added optional chaining in loader check (e.g., route?.options.loader). Control flow now guards for undefined routes or missing loaders.
Devtools AgeTicker: safe route access
packages/router-devtools-core/src/AgeTicker.tsx
Replaced non-null route assertion with optional access; updated condition to !route?.options.loader. Component returns null when route is absent or loader is missing.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor C as Caller
  participant R as Router
  participant LR as looseRoutesById

  C->>R: getHasLoader(routeId)
  R->>LR: route = LR[routeId]
  alt route missing or no loader
    R-->>C: true (no loader required)
  else loader present
    R-->>C: false (loader exists)
  end
Loading
sequenceDiagram
  autonumber
  participant UI as AgeTicker
  participant RT as Router
  participant LR as looseRoutesById

  UI->>RT: resolve match.routeId
  RT->>LR: route = LR[routeId]
  alt route missing or no loader
    UI-->>UI: render null
  else loader present
    UI-->>UI: render age ticker
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested reviewers

  • schiller-manuel

Poem

A whisk of code, a gentle hop,
I peek for loaders—if none, I stop.
No crash, no fuss, just burrow through,
Optional paths guide what we do.
With softer paws, I tread the route—
Safe checks in place, bugs shooed out! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Fix clearExpiredCache on removed routes" clearly and concisely summarizes the primary intent of the changes: prevent errors when routes are removed. It maps directly to the code edits that remove forced non-null assertions and add optional chaining to guard against missing routes, and it aligns with the PR description about routeTree updates in a Module Federation scenario.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@CC-Bell CC-Bell changed the title Fix clearExpiredCache on removed routes Fix clearExpiredCache/AgeTicker on removed routes Sep 24, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/router-core/src/router.ts (2)

2052-2056: Possible crash when calling hooks for exiting/staying/entering matches if a route was removed.

Accessing this.looseRoutesById[match.routeId]!.options[...] will throw when the route no longer exists (the exact scenario this PR targets). Use optional chaining to skip missing routes safely.

Apply this diff:

-                ).forEach(([matches, hook]) => {
-                  matches.forEach((match) => {
-                    this.looseRoutesById[match.routeId]!.options[hook]?.(match)
-                  })
-                })
+                ).forEach(([matches, hook]) => {
+                  matches.forEach((match) => {
+                    this.looseRoutesById[match.routeId]?.options[hook]?.(match)
+                  })
+                })

1-1: Fix unsafe non-null assertions for route lookups (looseRoutesById / routesById)

Multiple occurrences of non-null assertions remain — replace with guarded lookups or explicit handling to avoid runtime crashes.

  • packages/router-core/src/router.ts:1434, 1592, 2054
  • packages/router-core/src/ssr/ssr-client.ts:96, 104, 166
  • packages/router-core/src/load-matches.ts:164, 488, 723
  • packages/solid-router/src/HeadContent.tsx:103
  • packages/solid-router/src/Scripts.tsx:19
  • packages/react-router/src/HeadContent.tsx:105
  • packages/react-router/src/Scripts.tsx:19
  • (also: routesById non-null assertions) packages/solid-router/src/Match.tsx:216, 337; packages/react-router/src/Match.tsx:316
🧹 Nitpick comments (2)
packages/router-core/src/router.ts (2)

1434-1469: Optional: consider guarding route lookups consistently.

Elsewhere we still assume the route exists via non-null assertions. While typically safe for current matches resolved from the active route tree, using optional chaining makes hot-reload/module-federation updates more resilient.

Example:

-      const route = this.looseRoutesById[match.routeId]!
+      const route = this.looseRoutesById[match.routeId]
+      if (!route) return

1590-1594: Optional: safe access when mapping destination routes.

These are derived from current matching, so the route should exist. Still, optional chaining here would align with defensive handling in dynamic trees.

-      }).map((d) => this.looseRoutesById[d.routeId]!)
+      }).map((d) => this.looseRoutesById[d.routeId]).filter(Boolean) as Array<typeof this.flatRoutes[number]>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 065722a and 7aabcc9.

📒 Files selected for processing (2)
  • packages/router-core/src/router.ts (1 hunks)
  • packages/router-devtools-core/src/AgeTicker.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript in strict mode with extensive type safety across the codebase

Files:

  • packages/router-devtools-core/src/AgeTicker.tsx
  • packages/router-core/src/router.ts
packages/router-core/**

📄 CodeRabbit inference engine (AGENTS.md)

Keep framework-agnostic core router logic in packages/router-core/

Files:

  • packages/router-core/src/router.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
PR: TanStack/router#0
File: AGENTS.md:0-0
Timestamp: 2025-09-23T17:36:12.589Z
Learning: Applies to packages/router-core/** : Keep framework-agnostic core router logic in packages/router-core/
Learnt from: CR
PR: TanStack/router#0
File: AGENTS.md:0-0
Timestamp: 2025-09-23T17:36:12.589Z
Learning: Applies to packages/{router-devtools,*-router-devtools}/** : Keep router devtools packages in packages/router-devtools/ and packages/*-router-devtools/
🔇 Additional comments (3)
packages/router-core/src/router.ts (2)

2262-2267: Good fix: guard against missing routes in cache GC.

Switching to a safe lookup and early-evicting when the route is missing or has no loader prevents crashes when routes are removed dynamically.


2238-2256: Behavior note: filter semantics remain the same.

Returning true removes entries. This continues to eagerly drop cached matches for routes without loaders, which is consistent with previous behavior.

If intended, no action needed. If you want to retain non-loader routes in cache, invert the condition.

packages/router-devtools-core/src/AgeTicker.tsx (1)

45-49: LGTM once route narrowing is fixed.

Using defaults from the router options is correct; rendering null when no loader keeps devtools quiet for non-data routes or removed routes.

Comment on lines +38 to 41
const route = router().looseRoutesById[match.routeId]

if (!route.options.loader) {
if (!route?.options.loader) {
return null
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Type-safety: add explicit null check to narrow route.

if (!route?.options.loader) does not narrow route in TS strict mode, and route.options.* below will error. Guard route first.

Apply this diff:

-  const route = router().looseRoutesById[match.routeId]
-
-  if (!route?.options.loader) {
+  const route = router().looseRoutesById[match.routeId]
+  if (!route || !route.options.loader) {
     return null
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const route = router().looseRoutesById[match.routeId]
if (!route.options.loader) {
if (!route?.options.loader) {
return null
const route = router().looseRoutesById[match.routeId]
if (!route || !route.options.loader) {
return null
🤖 Prompt for AI Agents
In packages/router-devtools-core/src/AgeTicker.tsx around lines 38 to 41, the
current check uses optional chaining (if (!route?.options.loader)) which does
not narrow `route` under TS strict mode and will cause later accesses to
`route.options` to error; change the guard to first check for `route` explicitly
(for example: if (!route || !route.options.loader) return null) so TypeScript
narrows `route` and subsequent `route.options.*` access is safe.

const filter = (d: MakeRouteMatch<TRouteTree>) => {
const route = this.looseRoutesById[d.routeId]!
const route = this.looseRoutesById[d.routeId]

Copy link
Contributor

Choose a reason for hiding this comment

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

should we not just return here if route is undefined?

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