Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions packages/react-router/src/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,20 +128,27 @@ export function useLinkProps<
if (disabled) {
return undefined
}
let href = next.maskedLocation
? next.maskedLocation.url.href
: next.url.href

let external = false
if (router.origin) {
if (href.startsWith(router.origin)) {
href = router.history.createHref(href.replace(router.origin, '')) || '/'
} else {
external = true
}
// Use publicHref - it contains the correct href for display
// When a rewrite changes the origin, publicHref is the full URL
// Otherwise it's the origin-stripped path
// This avoids constructing URL objects in the hot path
const publicHref = next.maskedLocation
? next.maskedLocation.publicHref
: next.publicHref

// Check if publicHref is a full URL (different origin from rewrite)
const isFullUrl =
publicHref.startsWith('http://') || publicHref.startsWith('https://')
if (isFullUrl) {
// Full URL means rewrite changed the origin - treat as external-like
return { href: publicHref, external: false }
}

return {
href: router.history.createHref(publicHref) || '/',
external: false,
}
return { href, external }
}, [disabled, next.maskedLocation, next.url, router.origin, router.history])
}, [disabled, next.maskedLocation, next.publicHref, router.history])

const externalLink = React.useMemo(() => {
if (hrefOption?.external) {
Expand Down
69 changes: 47 additions & 22 deletions packages/router-core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1883,20 +1883,48 @@ export class RouterCore<
// Create the full path of the location
const fullPath = `${nextPathname}${searchStr}${hashStr}`

// Create the new href with full origin
const url = new URL(fullPath, this.origin)

// If a rewrite function is provided, use it to rewrite the URL
const rewrittenUrl = executeRewriteOutput(this.rewrite, url)
// Compute href and publicHref without URL construction when no rewrite
// URL is only constructed lazily when .url is accessed (for tests/edge cases)
let href: string
let publicHref: string
let _cachedUrl: URL | undefined

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, '')
// If rewrite changed the origin, publicHref needs full URL
// Otherwise just use the path components
if (rewrittenUrl.origin !== this.origin) {
publicHref = rewrittenUrl.href
} else {
publicHref =
rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash
}
_cachedUrl = rewrittenUrl
} else {
// Fast path: no rewrite, skip URL construction entirely
// fullPath is already the correct href (origin-stripped)
href = fullPath
publicHref = fullPath
}

// Use encoded URL path for href (consistent with parseLocation)
const encodedHref = url.href.replace(url.origin, '')
const origin = this.origin
const rewrite = this.rewrite

return {
publicHref:
rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash,
href: encodedHref,
url: rewrittenUrl,
publicHref,
href,
// Lazy URL getter - only constructs URL when accessed
// This eliminates URL construction from hot link rendering path
get url(): URL {
if (!_cachedUrl) {
const url = new URL(fullPath, origin)
_cachedUrl = rewrite ? executeRewriteOutput(rewrite, url) : url
}
return _cachedUrl
},
pathname: nextPathname,
search: nextSearch,
searchStr,
Expand Down Expand Up @@ -2203,8 +2231,9 @@ export class RouterCore<
// be a complete path (possibly with basepath)
if (to !== undefined || !href) {
const location = this.buildLocation({ to, ...rest } as any)
href = href ?? location.url.href
publicHref = publicHref ?? location.url.href
// Use publicHref which contains the path (origin-stripped is fine for reload)
href = href ?? location.publicHref
publicHref = publicHref ?? location.publicHref
}

// Use publicHref when available and href is not a full URL,
Expand Down Expand Up @@ -2279,10 +2308,9 @@ export class RouterCore<
_includeValidateSearch: true,
})

if (
this.latestLocation.publicHref !== nextLocation.publicHref ||
nextLocation.url.origin !== this.origin
) {
// Check if location changed - origin check is unnecessary since buildLocation
// always uses this.origin when constructing URLs
if (this.latestLocation.publicHref !== nextLocation.publicHref) {
const href = this.getParsedLocationHref(nextLocation)

throw redirect({ href })
Expand Down Expand Up @@ -2616,11 +2644,8 @@ export class RouterCore<
}

getParsedLocationHref = (location: ParsedLocation) => {
let href = location.url.href
if (this.origin && location.url.origin === this.origin) {
href = href.replace(this.origin, '') || '/'
}
return href
// location.href is already origin-stripped by buildLocation
return location.href || '/'
}

resolveRedirect = (redirect: AnyRedirect): AnyRedirect => {
Expand Down
31 changes: 17 additions & 14 deletions packages/solid-router/src/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,22 +137,25 @@ export function useLinkProps<
if (_options().disabled) {
return undefined
}
let href
const maskedLocation = next().maskedLocation
if (maskedLocation) {
href = maskedLocation.url.href
} else {
href = next().url.href
// Use publicHref - it contains the correct href for display
// When a rewrite changes the origin, publicHref is the full URL
// Otherwise it's the origin-stripped path
// This avoids constructing URL objects in the hot path
const location = next().maskedLocation ?? next()
const publicHref = location.publicHref

// Check if publicHref is a full URL (different origin from rewrite)
const isFullUrl =
publicHref.startsWith('http://') || publicHref.startsWith('https://')
if (isFullUrl) {
// Full URL means rewrite changed the origin - treat as external-like
return { href: publicHref, external: false }
}
let external = false
if (router.origin) {
if (href.startsWith(router.origin)) {
href = router.history.createHref(href.replace(router.origin, ''))
} else {
external = true
}

return {
href: router.history.createHref(publicHref) || '/',
external: false,
}
return { href, external }
})

const externalLink = Solid.createMemo(() => {
Expand Down
29 changes: 16 additions & 13 deletions packages/vue-router/src/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -471,23 +471,26 @@ export function useLinkProps<
return undefined
}
const nextLocation = next.value
const maskedLocation = nextLocation?.maskedLocation

let hrefValue: string
if (maskedLocation) {
hrefValue = maskedLocation.url.href
} else {
hrefValue = nextLocation?.url.href
const location = nextLocation?.maskedLocation ?? nextLocation

// Use publicHref - it contains the correct href for display
// When a rewrite changes the origin, publicHref is the full URL
// Otherwise it's the origin-stripped path
// This avoids constructing URL objects in the hot path
const publicHref = location?.publicHref
if (!publicHref) {
return undefined
}

// Handle origin stripping like Solid does
if (router.origin && hrefValue?.startsWith(router.origin)) {
hrefValue = router.history.createHref(
hrefValue.replace(router.origin, ''),
)
// Check if publicHref is a full URL (different origin from rewrite)
const isFullUrl =
publicHref.startsWith('http://') || publicHref.startsWith('https://')
if (isFullUrl) {
// Full URL means rewrite changed the origin
return publicHref
}

return hrefValue
return router.history.createHref(publicHref) || '/'
})

// Create static event handlers that don't change between renders
Expand Down
Loading