Skip to content

Conversation

@Sheraff
Copy link
Contributor

@Sheraff Sheraff commented Jan 21, 2026

URL objects are expensive to create and to update. Ideally we should minimize their use in the hot path. This PR proposes 2 changes:

  • in buildLocation skip creating a URL if there is no rewrite
  • during router initialization, skip creating a rewrite if there is no user-provided basepath nor any options.rewrite

Profiling: The purple blocks are URL > parse and set pathname > update. They are quite expensive to run, and completely gone after this PR (in the specific simple case we're testing, i.e. no rewrite, no basepath)

Before
Screenshot 2026-01-24 at 10 20 14

After
Screenshot 2026-01-24 at 10 20 26

Summary by CodeRabbit

  • Refactor

    • Public navigation surface simplified: location objects now expose href and publicHref and an external flag; internal URL objects removed.
    • Link behavior unified: components use publicHref for external/display URLs and router-created hrefs for in-app navigation.
  • Tests

    • Test assertions updated to reference href/publicHref and external semantics instead of internal URL objects.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

The PR replaces propagated URL objects with string-based href/publicHref and an external: boolean on ParsedLocation, updates RouterCore to emit href/publicHref instead of url, and updates framework link components and tests to use publicHref and router.history.createHref for internal links.

Changes

Cohort / File(s) Summary
Core Location Shape & Routing
packages/router-core/src/router.ts, packages/router-core/src/location.ts
buildLocation/parse now produce href and publicHref and an external: boolean instead of propagating a URL object. History payloads, navigate/redirect, helpers, and comparisons now use publicHref.
React Link
packages/react-router/src/link.tsx
useLinkProps reads maskedLocation ?? next, uses location.publicHref/external; external returns { href: publicHref, external: true }, otherwise returns router.history.createHref(publicHref). Memo deps updated.
Solid Link
packages/solid-router/src/link.tsx
Same refactor: compute from next().maskedLocation ?? next(), return publicHref for external, otherwise router.history.createHref(publicHref).
Vue Link
packages/vue-router/src/link.tsx
useLinkProps switched to location?.publicHref and external checks; external returns publicHref, internal uses `router.history.createHref(publicHref)
Tests
packages/*/tests/* (React, Solid, Vue)
Tests updated to stop constructing URL objects from location.url; assertions now use location.href/publicHref. Redirect tests expect external: false and no url field in _fromLocation.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant LinkComponent
  participant RouterCore
  participant RouterHistory
  participant BrowserHistory

  Client->>LinkComponent: render / click <to>
  LinkComponent->>RouterCore: call next() -> maskedLocation ?? next()
  RouterCore->>RouterCore: derive publicHref, href, external
  alt external === true
    LinkComponent->>Client: return { href: publicHref, external: true }
  else
    LinkComponent->>RouterHistory: router.history.createHref(publicHref)
    RouterHistory->>LinkComponent: created href (path)
    LinkComponent->>BrowserHistory: navigation (push/replace href)
    BrowserHistory->>RouterCore: commit location (payload uses publicHref)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • schiller-manuel
  • nlynzaad

Poem

🐰 I hopped from URL objects to a tidy string,
publicHref now makes my heart sing.
External flags wave from far-off skies,
Links resolve cleanly before my eyes.
Carrot-coded joy — a rabbit’s tiny spring.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main refactoring objective: skipping the URL constructor in buildLocation across router-core, which is the central change affecting multiple packages.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

@nx-cloud
Copy link

nx-cloud bot commented Jan 21, 2026

View your CI Pipeline Execution ↗ for commit 1b0c534

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 12m 30s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 42s View ↗

☁️ Nx Cloud last updated this comment at 2026-01-24 10:42:25 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 21, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@6447

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@6447

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@6447

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/nitro-v2-vite-plugin@6447

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@6447

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@6447

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-ssr-query@6447

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@6447

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@6447

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@6447

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@6447

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@6447

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@6447

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@6447

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@6447

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@6447

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-ssr-query-core@6447

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@6447

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@6447

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@6447

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@6447

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-ssr-query@6447

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@6447

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@6447

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@6447

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@6447

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-fn-stubs@6447

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@6447

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@6447

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-static-server-functions@6447

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@6447

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@6447

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@6447

@tanstack/vue-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-router@6447

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-router-devtools@6447

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-router-ssr-query@6447

@tanstack/vue-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start@6447

@tanstack/vue-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start-client@6447

@tanstack/vue-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start-server@6447

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@6447

commit: 1b0c534

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: 4

🤖 Fix all issues with AI agents
In `@packages/react-router/src/link.tsx`:
- Around line 131-149: The code currently treats full publicHref values as
internal by returning { href: publicHref, external: false }; update the handler
in link.tsx (the block that computes publicHref and isFullUrl) to mark full-URL
publicHref values as external (set external: true) so the Link component does
not intercept clicks for cross-origin rewrites; keep href as publicHref for
those cases and leave the router.history.createHref branch unchanged for
origin-stripped paths.

In `@packages/router-core/src/router.ts`:
- Around line 2646-2649: getParsedLocationHref currently returns an
origin-stripped location.href which loses cross-origin rewrite targets; change
getParsedLocationHref (used by resolveRedirect) to prefer the full URL (e.g.,
location.publicHref or location.url.href) and only strip the origin when it
exactly matches this.origin so same-origin behavior is preserved while
cross-origin redirects retain their full href; update any callers (notably
resolveRedirect) to use the new logic to emit full cross-origin URLs and keep
the existing same-origin stripping behavior.

In `@packages/solid-router/src/link.tsx`:
- Around line 140-158: The code treats full publicHref values as internal;
change the branch that detects full URLs (the isFullUrl check using
publicHref.startsWith('http://')/('https://')) to mark them external by
returning { href: publicHref, external: true } so SPA interception is disabled
for cross-origin rewrites; keep the other branch using
router.history.createHref(publicHref) for non-full paths.

In `@packages/vue-router/src/link.tsx`:
- Around line 474-493: The computed publicHref can be an absolute URL even for
an internal options.to, which risks client-side navigation attempting a
cross-origin push; update the href resolution and click handling so that when
publicHref is detected as a full URL (the logic in the block using
nextLocation?.maskedLocation ?? nextLocation and const publicHref =
location?.publicHref), you treat it as external: return the full publicHref and
signal/mark it as external (or short-circuit handleClick) so that handleClick
(the component's click handler that currently calls
router.navigate/router.history.createHref) does not attempt router.navigate or a
client-side push for that link; concretely, keep the isFullUrl detection, ensure
the resolver returns the absolute URL and modify handleClick to check the same
isFullUrl condition (or an external flag set alongside createHref) and fall back
to native navigation instead of calling router.navigate.

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

🤖 Fix all issues with AI agents
In `@packages/router-core/src/router.ts`:
- Around line 1885-1912: The internal href must be derived from fullPath before
running executeRewriteOutput to avoid in-place mutation of the URL changing our
internal path; change the block where this.rewrite is handled so href = fullPath
(or otherwise capture the origin-stripped path) before calling
executeRewriteOutput(this.rewrite, url), then compute publicHref from
rewrittenUrl (using rewrittenUrl.href when origins differ, or
rewrittenUrl.pathname+search+hash when same) so publicHref reflects the rewrite
while href remains the original internal path used by isSameUrl and redirects.

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: 0

Caution

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

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

2218-2226: Guard against cross‑origin rewrites during imperative navigation.
buildLocation now marks external when rewrite output changes origin. navigate({ to }) still proceeds with client-side commit and may hit a pushState cross-origin error. Consider forcing reloadDocument when location.external and using publicHref.

🛠️ Suggested fix
   navigate: NavigateFn = async ({
     to,
     reloadDocument,
     href,
     publicHref,
     ...rest
   }) => {
     let hrefIsUrl = false

     if (href) {
       try {
         new URL(`${href}`)
         hrefIsUrl = true
       } catch {}
     }

     if (hrefIsUrl && !reloadDocument) {
       reloadDocument = true
     }
+
+    let resolvedLocation: ParsedLocation | undefined
+    if (!reloadDocument && (to !== undefined || !href)) {
+      resolvedLocation = this.buildLocation({ to, ...rest } as any)
+      if (resolvedLocation.external) {
+        reloadDocument = true
+        href = href ?? resolvedLocation.publicHref
+        publicHref = publicHref ?? resolvedLocation.publicHref
+      }
+    }

     if (reloadDocument) {
       // When to is provided, always build a location to get the proper publicHref
       // (this handles redirects where href might be an internal path from resolveRedirect)
       // When only href is provided (no to), use it directly as it should already
       // be a complete path (possibly with basepath)
       if (to !== undefined || !href) {
-        const location = this.buildLocation({ to, ...rest } as any)
+        const location = resolvedLocation ?? this.buildLocation({ to, ...rest } as any)
         // Use publicHref which contains the path (origin-stripped is fine for reload)
         href = href ?? location.publicHref
         publicHref = publicHref ?? location.publicHref
       }
♻️ Duplicate comments (1)
packages/router-core/src/router.ts (1)

1886-1910: Keep internal href stable when rewrite output mutates the URL.
executeRewriteOutput can mutate url in-place; using url.href after rewrite can turn the internal href into the public path and break isSameUrl comparisons. Prefer the pre‑rewrite fullPath for href.

🛠️ Suggested fix
       if (this.rewrite) {
         // With rewrite, we need to construct URL to apply the rewrite
         const url = new URL(fullPath, this.origin)
         const rewrittenUrl = executeRewriteOutput(this.rewrite, url)
-        href = url.href.replace(url.origin, '')
+        // Keep internal href stable even if rewrite.output mutates `url`
+        href = fullPath
         // If rewrite changed the origin, publicHref needs full URL
         // Otherwise just use the path components
         if (rewrittenUrl.origin !== this.origin) {
           publicHref = rewrittenUrl.href
           external = true

Also applies to: 1912-1921

@dsonet
Copy link

dsonet commented Jan 22, 2026

Less objects created means less memory usage.

@Sheraff
Copy link
Contributor Author

Sheraff commented Jan 22, 2026

Less objects created means less memory usage.

i agree, but if the changes aren't reflected in measurable metrics, then something must be wrong

@Sheraff Sheraff merged commit cfbbcd5 into main Jan 24, 2026
6 checks passed
@Sheraff Sheraff deleted the refactor-router-core-skip-url-constructor-in-build-location branch January 24, 2026 10:42
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.

3 participants