Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
36 changes: 36 additions & 0 deletions packages/react-router/tests/route.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,42 @@ describe('getRouteApi', () => {
const api = getRouteApi('foo')
expect(api.useNavigate).toBeDefined()
})

it('should have the fullPath property', () => {
const api = getRouteApi('/posts')
expect(api.fullPath).toBe('/posts')
})

it('should have the to property', () => {
const api = getRouteApi('/posts')
expect(api.to).toBe('/posts')
})

it('fullPath should equal id for standard routes', () => {
const api = getRouteApi('/invoices/$invoiceId')
expect(api.fullPath).toBe('/invoices/$invoiceId')
expect(api.to).toBe('/invoices/$invoiceId')
expect(api.fullPath).toBe(api.id)
})

it('fullPath should differ from id for pathless layout routes', () => {
const rootRoute = createRootRoute()
const layoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: '_layout',
})
const postsRoute = createRoute({
getParentRoute: () => layoutRoute,
path: 'posts',
})
const routeTree = rootRoute.addChildren([layoutRoute.addChildren([postsRoute])])
createRouter({ routeTree, history })

const api = getRouteApi('/_layout/posts')
expect(api.id).toBe('/_layout/posts')
expect(api.fullPath).toBe('/posts')
expect(api.to).toBe('/posts')
})
})

describe('createRoute has the same hooks as getRouteApi', () => {
Expand Down
44 changes: 44 additions & 0 deletions packages/react-router/tests/routeApi.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,19 @@ const invoiceRoute = createRoute({
loader: () => ({ data: 0 }),
})

const layoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: '_layout',
})

const postsRoute = createRoute({
getParentRoute: () => layoutRoute,
path: 'posts',
})

const routeTree = rootRoute.addChildren([
invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
layoutRoute.addChildren([postsRoute]),
indexRoute,
])

Expand Down Expand Up @@ -94,6 +105,39 @@ describe('getRouteApi', () => {
LinkComponentRoute<'/invoices/$invoiceId'>
>()
})
test('fullPath', () => {
expectTypeOf(invoiceRouteApi.fullPath).toEqualTypeOf<'/invoices/$invoiceId'>()
})
test('to', () => {
expectTypeOf(invoiceRouteApi.to).toEqualTypeOf<'/invoices/$invoiceId'>()
})
test('id', () => {
expectTypeOf(invoiceRouteApi.id).toEqualTypeOf<'/invoices/$invoiceId'>()
})
})

describe('getRouteApi with pathless layout route', () => {
const postsRouteApi = getRouteApi<'/_layout/posts', DefaultRouter>(
'/_layout/posts',
)

test('id includes the layout segment', () => {
expectTypeOf(postsRouteApi.id).toEqualTypeOf<'/_layout/posts'>()
})

test('fullPath excludes the pathless layout segment', () => {
expectTypeOf(postsRouteApi.fullPath).toEqualTypeOf<'/posts'>()
})

test('to excludes the pathless layout segment', () => {
expectTypeOf(postsRouteApi.to).toEqualTypeOf<'/posts'>()
})

test('fullPath is a valid RoutePaths type for Link from prop', () => {
// Verify fullPath is assignable to RoutePaths (valid for Link's from prop)
type RoutePaths = '/posts' | '/invoices' | '/invoices/$invoiceId' | '/'
expectTypeOf(postsRouteApi.fullPath).toMatchTypeOf<RoutePaths>()
})
})

describe('createRoute', () => {
Expand Down
22 changes: 22 additions & 0 deletions packages/router-core/src/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1912,6 +1912,28 @@ export class BaseRouteApi<TId, TRouter extends AnyRouter = RegisteredRouter> {
this.id = id as any
}

/**
* The full path of the route, which can be used as the `from` parameter
* in navigation APIs like `<Link from={routeApi.fullPath}>` or `navigate({ from: routeApi.fullPath })`.
*/
get fullPath(): RouteTypesById<TRouter, TId>['fullPath'] {
if (typeof window !== 'undefined' && window.__TSR_ROUTER__) {
Copy link
Contributor

Choose a reason for hiding this comment

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

this doesnt work during SSR correctly, so we cant use this

Copy link
Author

@imsherrill imsherrill Jan 14, 2026

Choose a reason for hiding this comment

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

yep makes sense, i looked into it and i dont see a quick win on this one. maybe i should close this out entirely?

const route = window.__TSR_ROUTER__.routesById[this.id as string]
if (route) {
return route.fullPath as RouteTypesById<TRouter, TId>['fullPath']
}
}
return this.id as RouteTypesById<TRouter, TId>['fullPath']
}
Comment on lines +1915 to +1927
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for __TSR_ROUTER__ type declaration and definition
rg -n "__TSR_ROUTER__" --type=ts -B2 -A2 | head -100

Repository: TanStack/router

Length of output: 4492


🏁 Script executed:

# Look at the actual file to understand the context better
head -n 1940 packages/router-core/src/route.ts | tail -n 50

Repository: TanStack/router

Length of output: 1403


🏁 Script executed:

# Search for fullPath property definition and assignments
rg -n "fullPath" packages/router-core/src/ --type=ts -B1 -A1 | head -150

Repository: TanStack/router

Length of output: 9142


🏁 Script executed:

# Search for documentation about BaseRouteApi.fullPath or routeApi behavior
rg -n "routeApi\.fullPath|BaseRouteApi" --type=ts packages/router-core/src/ -B3 -A3 | head -80

Repository: TanStack/router

Length of output: 1529


🏁 Script executed:

# Check if there are any test cases that document this behavior
rg -n "fullPath" packages/router-core/tests/ --type=ts -B2 -A2 | head -100

Repository: TanStack/router

Length of output: 7889


🏁 Script executed:

# Search for tests involving routeApi or BaseRouteApi
rg -n "routeApi|BaseRouteApi" packages/router-core/tests/ --type=ts -B2 -A2 | head -100

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Look for tests that verify fullPath getter specifically
rg -n "\.fullPath" packages/router-core/tests/ --type=ts -B1 -A1 | head -50

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Search for usage of routeApi.fullPath or routeApi.to in the codebase
rg -n "routeApi\.(fullPath|to)" --type=ts --type=tsx | head -50

Repository: TanStack/router

Length of output: 86


🏁 Script executed:

# Look for any examples or tests that use BaseRouteApi
find packages -name "*.test.ts" -o -name "*.test.tsx" -o -name "*.spec.ts" | xargs grep -l "fullPath" | head -5

Repository: TanStack/router

Length of output: 289


🏁 Script executed:

# Check if there's SSR-specific documentation or comments about BaseRouteApi
rg -n "SSR|server.*render|isomorphic" packages/router-core/src/route.ts -B2 -A2 | head -50

Repository: TanStack/router

Length of output: 1425


🏁 Script executed:

# Look at the broader context of how fullPath is used in actual route implementation vs API
rg -n "class Route|class BaseRouteApi" packages/router-core/src/route.ts -A15 | grep -A15 "get fullPath"

Repository: TanStack/router

Length of output: 375


🏁 Script executed:

# Check if there's any documentation about BaseRouteApi in comments or README
rg -n "BaseRouteApi" packages/ -B3 -A3 --type=ts | head -80

Repository: TanStack/router

Length of output: 3463


🏁 Script executed:

# Search for any JSDoc updates or issues related to fullPath fallback behavior
rg -n "id.*fallback|fallback.*id" packages/router-core/src/ --type=ts -B2 -A2

Repository: TanStack/router

Length of output: 1464


Update JSDoc to document SSR fallback behavior.

The implementation correctly guards against SSR and falls back to id when the router isn't available. However, the JSDoc comment (lines 1915-1917) only describes the intended purpose and doesn't document the runtime limitation. For SSR or before createRouter completes, pathless layout routes will return their id (e.g., __root_/posts) instead of their computed fullPath (e.g., /posts). Add a note to the JSDoc explaining this behavior and when it occurs, so developers understand the constraint when using routeApi.fullPath in SSR contexts.

🤖 Prompt for AI Agents
In @packages/router-core/src/route.ts around lines 1915 - 1927, Update the JSDoc
for the fullPath getter to document its SSR/runtime fallback: note that fullPath
(getter fullPath on the route API) returns the computed route.fullPath when
window.__TSR_ROUTER__ and routesById[this.id] are available at runtime, but
during SSR or before createRouter completes (i.e., when window.__TSR_ROUTER__ is
absent) layout routes without a path will return this.id (e.g., "__root_/posts")
instead of a resolved path (e.g., "/posts"); mention when this limitation occurs
and suggest checking for a router instance or using client-side code if the
resolved path is required.


/**
* The `to` path of the route, an alias for `fullPath` that can be used
* for navigation. This provides parity with the `Route.to` property.
*/
get to(): RouteTypesById<TRouter, TId>['fullPath'] {
return this.fullPath
}

notFound = (opts?: NotFoundError) => {
return notFound({ routeId: this.id as string, ...opts })
}
Expand Down
36 changes: 36 additions & 0 deletions packages/solid-router/tests/route.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,42 @@ describe('getRouteApi', () => {
const api = getRouteApi('foo')
expect(api.useNavigate).toBeDefined()
})

it('should have the fullPath property', () => {
const api = getRouteApi('/posts')
expect(api.fullPath).toBe('/posts')
})

it('should have the to property', () => {
const api = getRouteApi('/posts')
expect(api.to).toBe('/posts')
})

it('fullPath should equal id for standard routes', () => {
const api = getRouteApi('/invoices/$invoiceId')
expect(api.fullPath).toBe('/invoices/$invoiceId')
expect(api.to).toBe('/invoices/$invoiceId')
expect(api.fullPath).toBe(api.id)
})

it('fullPath should differ from id for pathless layout routes', () => {
const rootRoute = createRootRoute()
const layoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: '_layout',
})
const postsRoute = createRoute({
getParentRoute: () => layoutRoute,
path: 'posts',
})
const routeTree = rootRoute.addChildren([layoutRoute.addChildren([postsRoute])])
createRouter({ routeTree, history })

const api = getRouteApi('/_layout/posts')
expect(api.id).toBe('/_layout/posts')
expect(api.fullPath).toBe('/posts')
expect(api.to).toBe('/posts')
})
})

describe('createRoute has the same hooks as getRouteApi', () => {
Expand Down
38 changes: 38 additions & 0 deletions packages/solid-router/tests/routeApi.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,19 @@ const invoiceRoute = createRoute({
loader: () => ({ data: 0 }),
})

const layoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: '_layout',
})

const postsRoute = createRoute({
getParentRoute: () => layoutRoute,
path: 'posts',
})

const routeTree = rootRoute.addChildren([
invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
layoutRoute.addChildren([postsRoute]),
indexRoute,
])

Expand Down Expand Up @@ -105,6 +116,33 @@ describe('getRouteApi', () => {
LinkComponentRoute<'/invoices/$invoiceId'>
>()
})
test('fullPath', () => {
expectTypeOf(invoiceRouteApi.fullPath).toEqualTypeOf<'/invoices/$invoiceId'>()
})
test('to', () => {
expectTypeOf(invoiceRouteApi.to).toEqualTypeOf<'/invoices/$invoiceId'>()
})
test('id', () => {
expectTypeOf(invoiceRouteApi.id).toEqualTypeOf<'/invoices/$invoiceId'>()
})
})

describe('getRouteApi with pathless layout route', () => {
const postsRouteApi = getRouteApi<'/_layout/posts', DefaultRouter>(
'/_layout/posts',
)

test('id includes the layout segment', () => {
expectTypeOf(postsRouteApi.id).toEqualTypeOf<'/_layout/posts'>()
})

test('fullPath excludes the pathless layout segment', () => {
expectTypeOf(postsRouteApi.fullPath).toEqualTypeOf<'/posts'>()
})

test('to excludes the pathless layout segment', () => {
expectTypeOf(postsRouteApi.to).toEqualTypeOf<'/posts'>()
})
})

describe('createRoute', () => {
Expand Down
36 changes: 36 additions & 0 deletions packages/vue-router/tests/route.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,42 @@ describe('getRouteApi', () => {
const api = getRouteApi('foo')
expect(api.useNavigate).toBeDefined()
})

it('should have the fullPath property', () => {
const api = getRouteApi('/posts')
expect(api.fullPath).toBe('/posts')
})

it('should have the to property', () => {
const api = getRouteApi('/posts')
expect(api.to).toBe('/posts')
})

it('fullPath should equal id for standard routes', () => {
const api = getRouteApi('/invoices/$invoiceId')
expect(api.fullPath).toBe('/invoices/$invoiceId')
expect(api.to).toBe('/invoices/$invoiceId')
expect(api.fullPath).toBe(api.id)
})

it('fullPath should differ from id for pathless layout routes', () => {
const rootRoute = createRootRoute()
const layoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: '_layout',
})
const postsRoute = createRoute({
getParentRoute: () => layoutRoute,
path: 'posts',
})
const routeTree = rootRoute.addChildren([layoutRoute.addChildren([postsRoute])])
createRouter({ routeTree, history })

const api = getRouteApi('/_layout/posts')
expect(api.id).toBe('/_layout/posts')
expect(api.fullPath).toBe('/posts')
expect(api.to).toBe('/posts')
})
})

describe('createRoute has the same hooks as getRouteApi', () => {
Expand Down
38 changes: 38 additions & 0 deletions packages/vue-router/tests/routeApi.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,19 @@ const invoiceRoute = createRoute({
loader: () => ({ data: 0 }),
})

const layoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: '_layout',
})

const postsRoute = createRoute({
getParentRoute: () => layoutRoute,
path: 'posts',
})

const routeTree = rootRoute.addChildren([
invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
layoutRoute.addChildren([postsRoute]),
indexRoute,
])

Expand Down Expand Up @@ -96,6 +107,33 @@ describe('getRouteApi', () => {
Vue.Ref<MakeRouteMatch<typeof routeTree, '/invoices/$invoiceId'>>
>()
})
test('fullPath', () => {
expectTypeOf(invoiceRouteApi.fullPath).toEqualTypeOf<'/invoices/$invoiceId'>()
})
test('to', () => {
expectTypeOf(invoiceRouteApi.to).toEqualTypeOf<'/invoices/$invoiceId'>()
})
test('id', () => {
expectTypeOf(invoiceRouteApi.id).toEqualTypeOf<'/invoices/$invoiceId'>()
})
})

describe('getRouteApi with pathless layout route', () => {
const postsRouteApi = getRouteApi<'/_layout/posts', DefaultRouter>(
'/_layout/posts',
)

test('id includes the layout segment', () => {
expectTypeOf(postsRouteApi.id).toEqualTypeOf<'/_layout/posts'>()
})

test('fullPath excludes the pathless layout segment', () => {
expectTypeOf(postsRouteApi.fullPath).toEqualTypeOf<'/posts'>()
})

test('to excludes the pathless layout segment', () => {
expectTypeOf(postsRouteApi.to).toEqualTypeOf<'/posts'>()
})
})

describe('createRoute', () => {
Expand Down