Skip to content

Commit 5eeb123

Browse files
chorobinTkDodo
andauthored
feat: add bindings for Route.Link and routeApi.Link (#4095)
This allows `Route.Link` and `routeApi.Link` bindings instead of manual `from`. We never used to be able to do this due to circular issues but it seems this is no longer an issue since all the changes to the types --------- Co-authored-by: TkDodo <[email protected]>
1 parent f0dbf0a commit 5eeb123

File tree

6 files changed

+87
-6
lines changed

6 files changed

+87
-6
lines changed

packages/react-router/src/link.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,9 +452,12 @@ export type CreateLinkProps = LinkProps<
452452
string
453453
>
454454

455-
export type LinkComponent<TComp> = <
455+
export type LinkComponent<
456+
in out TComp,
457+
in out TDefaultFrom extends string = string,
458+
> = <
456459
TRouter extends AnyRouter = RegisteredRouter,
457-
const TFrom extends string = string,
460+
const TFrom extends string = TDefaultFrom,
458461
const TTo extends string | undefined = undefined,
459462
const TMaskFrom extends string = TFrom,
460463
const TMaskTo extends string = '',

packages/react-router/src/route.ts renamed to packages/react-router/src/route.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import {
44
BaseRouteApi,
55
notFound,
66
} from '@tanstack/router-core'
7+
import React from 'react'
78
import { useLoaderData } from './useLoaderData'
89
import { useLoaderDeps } from './useLoaderDeps'
910
import { useParams } from './useParams'
1011
import { useSearch } from './useSearch'
1112
import { useNavigate } from './useNavigate'
1213
import { useMatch } from './useMatch'
1314
import { useRouter } from './useRouter'
15+
import { Link } from './link'
1416
import type {
1517
AnyContext,
1618
AnyRoute,
@@ -39,8 +41,8 @@ import type { UseMatchRoute } from './useMatch'
3941
import type { UseLoaderDepsRoute } from './useLoaderDeps'
4042
import type { UseParamsRoute } from './useParams'
4143
import type { UseSearchRoute } from './useSearch'
42-
import type * as React from 'react'
4344
import type { UseRouteContextRoute } from './useRouteContext'
45+
import type { LinkComponent } from './link'
4446

4547
declare module '@tanstack/router-core' {
4648
export interface UpdatableRouteOptionsExtensions {
@@ -61,6 +63,7 @@ declare module '@tanstack/router-core' {
6163
useLoaderDeps: UseLoaderDepsRoute<TId>
6264
useLoaderData: UseLoaderDataRoute<TId>
6365
useNavigate: () => UseNavigateResult<TFullPath>
66+
Link: LinkComponent<'a', TFullPath>
6467
}
6568
}
6669

@@ -133,6 +136,16 @@ export class RouteApi<
133136
notFound = (opts?: NotFoundError) => {
134137
return notFound({ routeId: this.id as string, ...opts })
135138
}
139+
140+
Link: LinkComponent<'a', RouteTypesById<TRouter, TId>['fullPath']> =
141+
React.forwardRef((props, ref: React.ForwardedRef<HTMLAnchorElement>) => {
142+
const router = useRouter()
143+
const fullPath = router.routesById[this.id as string].fullPath
144+
return <Link ref={ref} from={fullPath as never} {...props} />
145+
}) as unknown as LinkComponent<
146+
'a',
147+
RouteTypesById<TRouter, TId>['fullPath']
148+
>
136149
}
137150

138151
export class Route<
@@ -241,6 +254,19 @@ export class Route<
241254
useNavigate = (): UseNavigateResult<TFullPath> => {
242255
return useNavigate({ from: this.fullPath })
243256
}
257+
258+
Link: LinkComponent<'a', TFullPath> = React.forwardRef(
259+
(props, ref: React.ForwardedRef<HTMLAnchorElement>) => {
260+
const router = useRouter()
261+
return (
262+
<Link
263+
ref={ref}
264+
from={router.routesById[this.id].fullPath as never}
265+
{...props}
266+
/>
267+
)
268+
},
269+
) as unknown as LinkComponent<'a', TFullPath>
244270
}
245271

246272
export function createRoute<
@@ -426,6 +452,19 @@ export class RootRoute<
426452
useNavigate = (): UseNavigateResult<'/'> => {
427453
return useNavigate({ from: this.fullPath })
428454
}
455+
456+
Link: LinkComponent<'a', '/'> = React.forwardRef(
457+
(props, ref: React.ForwardedRef<HTMLAnchorElement>) => {
458+
const router = useRouter()
459+
return (
460+
<Link
461+
ref={ref}
462+
from={router.routesById[this.id].fullPath as never}
463+
{...props}
464+
/>
465+
)
466+
},
467+
) as unknown as LinkComponent<'a', '/'>
429468
}
430469

431470
export function createRootRoute<

packages/react-router/tests/routeApi.test-d.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expectTypeOf, test } from 'vitest'
22
import { createRootRoute, createRoute, createRouter, getRouteApi } from '../src'
3-
import type { MakeRouteMatch, UseNavigateResult } from '../src'
3+
import type { LinkComponent, MakeRouteMatch, UseNavigateResult } from '../src'
44

55
const rootRoute = createRootRoute()
66

@@ -87,6 +87,12 @@ describe('getRouteApi', () => {
8787
MakeRouteMatch<typeof routeTree, '/invoices/$invoiceId'>
8888
>()
8989
})
90+
test('Link', () => {
91+
const Link = invoiceRouteApi.Link
92+
expectTypeOf(Link).toEqualTypeOf<
93+
LinkComponent<'a', '/invoices/$invoiceId'>
94+
>()
95+
})
9096
})
9197

9298
describe('createRoute', () => {

packages/solid-router/src/link.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,9 +509,12 @@ export type CreateLinkProps = LinkProps<
509509
string
510510
>
511511

512-
export type LinkComponent<TComp> = <
512+
export type LinkComponent<
513+
in out TComp,
514+
in out TDefaultFrom extends string = string,
515+
> = <
513516
TRouter extends AnyRouter = RegisteredRouter,
514-
const TFrom extends string = string,
517+
const TFrom extends string = TDefaultFrom,
515518
const TTo extends string | undefined = undefined,
516519
const TMaskFrom extends string = TFrom,
517520
const TMaskTo extends string = '',

packages/solid-router/src/route.ts renamed to packages/solid-router/src/route.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
BaseRouteApi,
55
notFound,
66
} from '@tanstack/router-core'
7+
import { Link } from './link'
78
import { useLoaderData } from './useLoaderData'
89
import { useLoaderDeps } from './useLoaderDeps'
910
import { useParams } from './useParams'
@@ -41,6 +42,7 @@ import type { UseParamsRoute } from './useParams'
4142
import type { UseSearchRoute } from './useSearch'
4243
import type * as Solid from 'solid-js'
4344
import type { UseRouteContextRoute } from './useRouteContext'
45+
import type { LinkComponent } from './link'
4446

4547
declare module '@tanstack/router-core' {
4648
export interface UpdatableRouteOptionsExtensions {
@@ -61,6 +63,7 @@ declare module '@tanstack/router-core' {
6163
useLoaderDeps: UseLoaderDepsRoute<TId>
6264
useLoaderData: UseLoaderDataRoute<TId>
6365
useNavigate: () => UseNavigateResult<TFullPath>
66+
Link: LinkComponent<'a', TFullPath>
6467
}
6568
}
6669

@@ -128,6 +131,14 @@ export class RouteApi<
128131
notFound = (opts?: NotFoundError) => {
129132
return notFound({ routeId: this.id as string, ...opts })
130133
}
134+
135+
Link: LinkComponent<'a', RouteTypesById<TRouter, TId>['fullPath']> = (
136+
props,
137+
) => {
138+
const router = useRouter()
139+
const fullPath = router.routesById[this.id as string].fullPath
140+
return <Link from={fullPath as never} {...props} />
141+
}
131142
}
132143

133144
export class Route<
@@ -230,6 +241,12 @@ export class Route<
230241
useNavigate = (): UseNavigateResult<TFullPath> => {
231242
return useNavigate({ from: this.fullPath })
232243
}
244+
245+
Link: LinkComponent<'a', TFullPath> = (props) => {
246+
const router = useRouter()
247+
const fullPath = router.routesById[this.id as string].fullPath
248+
return <Link from={fullPath as never} {...props} />
249+
}
233250
}
234251

235252
export function createRoute<
@@ -411,6 +428,12 @@ export class RootRoute<
411428
useNavigate = (): UseNavigateResult<'/'> => {
412429
return useNavigate({ from: this.fullPath })
413430
}
431+
432+
Link: LinkComponent<'a', '/'> = (props) => {
433+
const router = useRouter()
434+
const fullPath = router.routesById[this.id as string].fullPath
435+
return <Link from={fullPath as never} {...props} />
436+
}
414437
}
415438

416439
export function createRouteMask<

packages/solid-router/tests/routeApi.test-d.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expectTypeOf, test } from 'vitest'
22
import { createRootRoute, createRoute, createRouter, getRouteApi } from '../src'
3+
import type { LinkComponent } from '../src'
34
import type { Accessor } from 'solid-js'
45
import type { MakeRouteMatch, UseNavigateResult } from '@tanstack/router-core'
56

@@ -96,6 +97,12 @@ describe('getRouteApi', () => {
9697
Accessor<MakeRouteMatch<typeof routeTree, '/invoices/$invoiceId'>>
9798
>()
9899
})
100+
test('Link', () => {
101+
const Link = invoiceRouteApi.Link
102+
expectTypeOf(Link).toEqualTypeOf<
103+
LinkComponent<'a', '/invoices/$invoiceId'>
104+
>()
105+
})
99106
})
100107

101108
describe('createRoute', () => {

0 commit comments

Comments
 (0)