Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b8af575
enhance relative navigation from determination
nlynzaad Aug 17, 2025
c53266f
update react-router tests
nlynzaad Aug 17, 2025
9e1b1dd
some more test refining
nlynzaad Aug 17, 2025
55ba9af
code cleanup
nlynzaad Aug 17, 2025
ba524f1
test cleanup
nlynzaad Aug 17, 2025
745107c
replicate changes to Solid
nlynzaad Aug 17, 2025
b4d41f8
update docs
nlynzaad Aug 17, 2025
44ad37c
ci: apply automated fixes
autofix-ci[bot] Aug 17, 2025
d5701da
Merge branch 'main' into relative-routing-to-current-location
nlynzaad Aug 17, 2025
e0b8814
resolve merge issue
nlynzaad Aug 17, 2025
0c9b67a
apply code rabbit suggestions
nlynzaad Aug 17, 2025
983b7ce
Merge branch 'main' into relative-routing-to-current-location
nlynzaad Aug 17, 2025
723540c
ci: apply automated fixes
autofix-ci[bot] Aug 17, 2025
944af34
apply code rabbit doc suggestion
nlynzaad Aug 17, 2025
f5c5559
apply code rabbit doc suggestion
nlynzaad Aug 17, 2025
0ae150e
reactivity and code cleanup
nlynzaad Aug 17, 2025
ad7eea5
ci: apply automated fixes
autofix-ci[bot] Aug 17, 2025
8f41f21
cleanup tests
nlynzaad Aug 18, 2025
de884b8
consolidate from logic
nlynzaad Aug 19, 2025
d3391ea
Merge branch 'main' into relative-routing-to-current-location
nlynzaad Aug 19, 2025
8feb3c0
revert change to SolidJS useNavigate
nlynzaad Aug 19, 2025
452f876
ci: apply automated fixes
autofix-ci[bot] Aug 19, 2025
41481f7
Merge branch 'main' into relative-routing-to-current-location
nlynzaad Aug 19, 2025
ab6f27b
rersolve inconsistency between solid and react implementation
nlynzaad Aug 19, 2025
3d93911
ci: apply automated fixes
autofix-ci[bot] Aug 19, 2025
87d51b9
formatting
nlynzaad Aug 19, 2025
29dfcf9
refactor based on coderabbit recommendations
nlynzaad Aug 19, 2025
9c46060
ci: apply automated fixes
autofix-ci[bot] Aug 19, 2025
3e66462
resolve test failure
nlynzaad Aug 19, 2025
508ee37
Merge branch 'main' into relative-routing-to-current-location
nlynzaad Aug 26, 2025
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
17 changes: 12 additions & 5 deletions docs/router/framework/react/guide/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type ToOptions<
TTo extends string = '',
> = {
// `from` is an optional route ID or path. If it is not supplied, only absolute paths will be auto-completed and type-safe. It's common to supply the route.fullPath of the origin route you are rendering from for convenience. If you don't know the origin route, leave this empty and work with absolute paths or unsafe relative paths.
from: string
from?: string
// `to` can be an absolute route path or a relative path from the `from` option to a valid route path. ⚠️ Do not interpolate path params, hash or search params into the `to` options. Use the `params`, `search`, and `hash` options instead.
to: string
// `params` is either an object of path params to interpolate into the `to` option or a function that supplies the previous params and allows you to return new ones. This is the only way to interpolate dynamic parameters into the final URL. Depending on the `from` and `to` route, you may need to supply none, some or all of the path params. TypeScript will notify you of the required params if there are any.
Expand Down Expand Up @@ -183,7 +183,7 @@ Keep in mind that normally dynamic segment params are `string` values, but they

By default, all links are absolute unless a `from` route path is provided. This means that the above link will always navigate to the `/about` route regardless of what route you are currently on.

If you want to make a link that is relative to the current route, you can provide a `from` route path:
Relative links can be combined with a `from` route path. If a from route path isn't provided, relative paths default to the current active location.

```tsx
const postIdRoute = createRoute({
Expand All @@ -201,9 +201,9 @@ As seen above, it's common to provide the `route.fullPath` as the `from` route p

### Special relative paths: `"."` and `".."`

Quite often you might want to reload the current location, for example, to rerun the loaders on the current and/or parent routes, or maybe there was a change in search parameters. This can be achieved by specifying a `to` route path of `"."` which will reload the current location. This is only applicable to the current location, and hence any `from` route path specified is ignored.
Quite often you might want to reload the current location or another `from` path, for example, to rerun the loaders on the current and/or parent routes, or maybe navigate back to a parent route. This can be achieved by specifying a `to` route path of `"."` which will reload the current location or provided `from` path.

Another common need is to navigate one route back relative to the current location or some other matched route in the current tree. By specifying a `to` route path of `".."` navigation will be resolved to either the first parent route preceding the current location or, if specified, preceding the `"from"` route path.
Another common need is to navigate one route back relative to the current location or another path. By specifying a `to` route path of `".."` navigation will be resolved to the first parent route preceding the current location.

```tsx
export const Route = createFileRoute('/posts/$postId')({
Expand All @@ -214,7 +214,14 @@ function PostComponent() {
return (
<div>
<Link to=".">Reload the current route of /posts/$postId</Link>
<Link to="..">Navigate to /posts</Link>
<Link to="..">Navigate back to /posts</Link>
// the below are all equivalent
<Link to="/posts">Navigate back to /posts</Link>
<Link from="/posts" to=".">
Navigate back to /posts
</Link>
// the below are all equivalent
<Link to="/">Navigate to root</Link>
<Link from="/posts" to="..">
Navigate to root
</Link>
Expand Down
102 changes: 46 additions & 56 deletions packages/react-router/src/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import {
preloadWarning,
removeTrailingSlash,
} from '@tanstack/router-core'
import { useActiveLocation } from './useActiveLocation'
import { useRouterState } from './useRouterState'
import { useRouter } from './useRouter'

import { useForwardedRef, useIntersectionObserver } from './utils'

import { useMatch } from './useMatch'
import type {
AnyRouter,
Constrain,
Expand Down Expand Up @@ -99,19 +99,27 @@ export function useLinkProps<
structuralSharing: true as any,
})

const from = useMatch({
strict: false,
select: (match) => options.from ?? match.fullPath,
// subscribe to location here to re-build fromPath if it changes
const routerLocation = useRouterState({
select: (s) => s.location,
structuralSharing: true as any,
})

const next = React.useMemo(
() => router.buildLocation({ ...options, from } as any),
const { getFromPath } = useActiveLocation()

const from = getFromPath(options.from)

const _options = React.useMemo(
() => {
return { ...options, from }
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
router,
routerLocation,
currentSearch,
options._fromLocation,
from,
options._fromLocation,
options.hash,
options.to,
options.search,
Expand All @@ -122,6 +130,11 @@ export function useLinkProps<
],
)

const next = React.useMemo(
() => router.buildLocation({ ..._options } as any),
[router, _options],
)

const isExternal = type === 'external'

const preload =
Expand Down Expand Up @@ -180,34 +193,12 @@ export function useLinkProps<
},
})

const doPreload = React.useCallback(
() => {
router.preloadRoute({ ...options, from } as any).catch((err) => {
console.warn(err)
console.warn(preloadWarning)
})
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
router,
options.to,
options._fromLocation,
from,
options.search,
options.hash,
options.params,
options.state,
options.mask,
options.unsafeRelative,
options.hashScrollIntoView,
options.href,
options.ignoreBlocker,
options.reloadDocument,
options.replace,
options.resetScroll,
options.viewTransition,
],
)
const doPreload = React.useCallback(() => {
router.preloadRoute({ ..._options } as any).catch((err) => {
console.warn(err)
console.warn(preloadWarning)
})
}, [router, _options])

const preloadViewportIoCallback = React.useCallback(
(entry: IntersectionObserverEntry | undefined) => {
Expand Down Expand Up @@ -235,25 +226,6 @@ export function useLinkProps<
}
}, [disabled, doPreload, preload])

if (isExternal) {
return {
...propsSafeToSpread,
ref: innerRef as React.ComponentPropsWithRef<'a'>['ref'],
type,
href: to,
...(children && { children }),
...(target && { target }),
...(disabled && { disabled }),
...(style && { style }),
...(className && { className }),
...(onClick && { onClick }),
...(onFocus && { onFocus }),
...(onMouseEnter && { onMouseEnter }),
...(onMouseLeave && { onMouseLeave }),
...(onTouchStart && { onTouchStart }),
}
}

// The click handler
const handleClick = (e: React.MouseEvent) => {
if (
Expand All @@ -277,8 +249,7 @@ export function useLinkProps<
// All is well? Navigate!
// N.B. we don't call `router.commitLocation(next) here because we want to run `validateSearch` before committing
router.navigate({
...options,
from,
..._options,
replace,
resetScroll,
hashScrollIntoView,
Expand All @@ -289,6 +260,25 @@ export function useLinkProps<
}
}

if (isExternal) {
return {
...propsSafeToSpread,
ref: innerRef as React.ComponentPropsWithRef<'a'>['ref'],
type,
href: to,
...(children && { children }),
...(target && { target }),
...(disabled && { disabled }),
...(style && { style }),
...(className && { className }),
...(onClick && { onClick }),
...(onFocus && { onFocus }),
...(onMouseEnter && { onMouseEnter }),
...(onMouseLeave && { onMouseLeave }),
...(onTouchStart && { onTouchStart }),
}
}

// The click handler
const handleFocus = (_: React.MouseEvent) => {
if (disabled) return
Expand Down
57 changes: 57 additions & 0 deletions packages/react-router/src/useActiveLocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { last } from '@tanstack/router-core'
import { useCallback, useEffect, useState } from 'react'
import { useRouter } from './useRouter'
import { useMatch } from './useMatch'
import { useRouterState } from './useRouterState'
import type { ParsedLocation } from '@tanstack/router-core'

export type UseActiveLocationResult = {
activeLocation: ParsedLocation
getFromPath: (from?: string) => string
setActiveLocation: (location?: ParsedLocation) => void
}

export const useActiveLocation = (
location?: ParsedLocation,
): UseActiveLocationResult => {
const router = useRouter()
const routerLocation = useRouterState({ select: (state) => state.location })
const [activeLocation, setActiveLocation] = useState<ParsedLocation>(
location ?? routerLocation,
)
const [customActiveLocation, setCustomActiveLocation] = useState<
ParsedLocation | undefined
>(location)

useEffect(() => {
setActiveLocation(customActiveLocation ?? routerLocation)
}, [routerLocation, customActiveLocation])

const matchIndex = useMatch({
strict: false,
select: (match) => match.index,
})

const getFromPath = useCallback(
(from?: string) => {
const activeLocationMatches = router.matchRoutes(activeLocation, {
_buildLocation: false,
})

const activeLocationMatch = last(activeLocationMatches)

return (
from ??
activeLocationMatch?.fullPath ??
router.state.matches[matchIndex]!.fullPath
)
},
[activeLocation, matchIndex, router],
)

return {
activeLocation,
getFromPath,
setActiveLocation: setCustomActiveLocation,
}
}
20 changes: 6 additions & 14 deletions packages/react-router/src/useNavigate.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react'
import { useRouter } from './useRouter'
import { useMatch } from './useMatch'
import { useActiveLocation } from './useActiveLocation'
import type {
AnyRouter,
FromPathOption,
Expand All @@ -15,29 +15,21 @@ export function useNavigate<
>(_defaultOpts?: {
from?: FromPathOption<TRouter, TDefaultFrom>
}): UseNavigateResult<TDefaultFrom> {
const { navigate, state } = useRouter()
const router = useRouter()

// Just get the index of the current match to avoid rerenders
// as much as possible
const matchIndex = useMatch({
strict: false,
select: (match) => match.index,
})
const { getFromPath, activeLocation } = useActiveLocation()

return React.useCallback(
(options: NavigateOptions) => {
const from =
options.from ??
_defaultOpts?.from ??
state.matches[matchIndex]!.fullPath
const from = getFromPath(options.from ?? _defaultOpts?.from)

return navigate({
return router.navigate({
...options,
from,
})
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[_defaultOpts?.from, navigate],
[_defaultOpts?.from, router, getFromPath, activeLocation],
) as UseNavigateResult<TDefaultFrom>
}

Expand Down
Loading
Loading