From 774063130003007145ee1cab53588e0278f3711e Mon Sep 17 00:00:00 2001 From: chorobin Date: Mon, 5 May 2025 23:46:48 +0200 Subject: [PATCH 1/3] feat: add bindings for `Route.Link` and `routeApi.Link` --- packages/react-router/src/link.tsx | 7 +++- .../react-router/src/{route.ts => route.tsx} | 41 ++++++++++++++++++- packages/solid-router/src/link.tsx | 7 +++- .../solid-router/src/{route.ts => route.tsx} | 23 +++++++++++ 4 files changed, 73 insertions(+), 5 deletions(-) rename packages/react-router/src/{route.ts => route.tsx} (91%) rename packages/solid-router/src/{route.ts => route.tsx} (93%) diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx index ae4fd238741..56fba11d386 100644 --- a/packages/react-router/src/link.tsx +++ b/packages/react-router/src/link.tsx @@ -452,9 +452,12 @@ export type CreateLinkProps = LinkProps< string > -export type LinkComponent = < +export type LinkComponent< + in out TComp, + in out TDefaultFrom extends string = string, +> = < TRouter extends AnyRouter = RegisteredRouter, - const TFrom extends string = string, + const TFrom extends string = TDefaultFrom, const TTo extends string | undefined = undefined, const TMaskFrom extends string = TFrom, const TMaskTo extends string = '', diff --git a/packages/react-router/src/route.ts b/packages/react-router/src/route.tsx similarity index 91% rename from packages/react-router/src/route.ts rename to packages/react-router/src/route.tsx index f9c8898cfe8..01b2caf8517 100644 --- a/packages/react-router/src/route.ts +++ b/packages/react-router/src/route.tsx @@ -4,6 +4,7 @@ import { BaseRouteApi, notFound, } from '@tanstack/router-core' +import React from 'react' import { useLoaderData } from './useLoaderData' import { useLoaderDeps } from './useLoaderDeps' import { useParams } from './useParams' @@ -11,6 +12,7 @@ import { useSearch } from './useSearch' import { useNavigate } from './useNavigate' import { useMatch } from './useMatch' import { useRouter } from './useRouter' +import { Link } from './link' import type { AnyContext, AnyRoute, @@ -39,8 +41,8 @@ import type { UseMatchRoute } from './useMatch' import type { UseLoaderDepsRoute } from './useLoaderDeps' import type { UseParamsRoute } from './useParams' import type { UseSearchRoute } from './useSearch' -import type * as React from 'react' import type { UseRouteContextRoute } from './useRouteContext' +import type { LinkComponent } from './link' declare module '@tanstack/router-core' { export interface UpdatableRouteOptionsExtensions { @@ -61,6 +63,7 @@ declare module '@tanstack/router-core' { useLoaderDeps: UseLoaderDepsRoute useLoaderData: UseLoaderDataRoute useNavigate: () => UseNavigateResult + Link: LinkComponent<'a', TFullPath> } } @@ -133,6 +136,16 @@ export class RouteApi< notFound = (opts?: NotFoundError) => { return notFound({ routeId: this.id as string, ...opts }) } + + Link: LinkComponent<'a', RouteTypesById['fullPath']> = + React.forwardRef((props, ref: React.ForwardedRef) => { + const router = useRouter() + const fullPath = router.routesById[this.id as string].fullPath + return + }) as unknown as LinkComponent< + 'a', + RouteTypesById['fullPath'] + > } export class Route< @@ -241,6 +254,19 @@ export class Route< useNavigate = (): UseNavigateResult => { return useNavigate({ from: this.fullPath }) } + + Link = React.forwardRef( + (props, ref: React.ForwardedRef) => { + const router = useRouter() + return ( + + ) + }, + ) as unknown as LinkComponent<'a', TFullPath> } export function createRoute< @@ -426,6 +452,19 @@ export class RootRoute< useNavigate = (): UseNavigateResult<'/'> => { return useNavigate({ from: this.fullPath }) } + + Link = React.forwardRef( + (props, ref: React.ForwardedRef) => { + const router = useRouter() + return ( + + ) + }, + ) as unknown as LinkComponent<'a', '/'> } export function createRootRoute< diff --git a/packages/solid-router/src/link.tsx b/packages/solid-router/src/link.tsx index 79e58b30a34..bc9e3f23030 100644 --- a/packages/solid-router/src/link.tsx +++ b/packages/solid-router/src/link.tsx @@ -509,9 +509,12 @@ export type CreateLinkProps = LinkProps< string > -export type LinkComponent = < +export type LinkComponent< + in out TComp, + in out TDefaultFrom extends string = string, +> = < TRouter extends AnyRouter = RegisteredRouter, - const TFrom extends string = string, + const TFrom extends string = TDefaultFrom, const TTo extends string | undefined = undefined, const TMaskFrom extends string = TFrom, const TMaskTo extends string = '', diff --git a/packages/solid-router/src/route.ts b/packages/solid-router/src/route.tsx similarity index 93% rename from packages/solid-router/src/route.ts rename to packages/solid-router/src/route.tsx index c56dc4ef8fa..1eb2701202e 100644 --- a/packages/solid-router/src/route.ts +++ b/packages/solid-router/src/route.tsx @@ -4,6 +4,7 @@ import { BaseRouteApi, notFound, } from '@tanstack/router-core' +import { Link } from './link' import { useLoaderData } from './useLoaderData' import { useLoaderDeps } from './useLoaderDeps' import { useParams } from './useParams' @@ -41,6 +42,7 @@ import type { UseParamsRoute } from './useParams' import type { UseSearchRoute } from './useSearch' import type * as Solid from 'solid-js' import type { UseRouteContextRoute } from './useRouteContext' +import type { LinkComponent } from './link' declare module '@tanstack/router-core' { export interface UpdatableRouteOptionsExtensions { @@ -61,6 +63,7 @@ declare module '@tanstack/router-core' { useLoaderDeps: UseLoaderDepsRoute useLoaderData: UseLoaderDataRoute useNavigate: () => UseNavigateResult + Link: LinkComponent<'a', TFullPath> } } @@ -128,6 +131,14 @@ export class RouteApi< notFound = (opts?: NotFoundError) => { return notFound({ routeId: this.id as string, ...opts }) } + + Link: LinkComponent<'a', RouteTypesById['fullPath']> = ( + props, + ) => { + const router = useRouter() + const fullPath = router.routesById[this.id as string].fullPath + return + } } export class Route< @@ -230,6 +241,12 @@ export class Route< useNavigate = (): UseNavigateResult => { return useNavigate({ from: this.fullPath }) } + + Link: LinkComponent<'a', TFullPath> = (props) => { + const router = useRouter() + const fullPath = router.routesById[this.id as string].fullPath + return + } } export function createRoute< @@ -411,6 +428,12 @@ export class RootRoute< useNavigate = (): UseNavigateResult<'/'> => { return useNavigate({ from: this.fullPath }) } + + Link: LinkComponent<'a', '/'> = (props) => { + const router = useRouter() + const fullPath = router.routesById[this.id as string].fullPath + return + } } export function createRouteMask< From 35f706c147c25196ea2974f35611162b8e574c83 Mon Sep 17 00:00:00 2001 From: chorobin Date: Tue, 6 May 2025 00:04:50 +0200 Subject: [PATCH 2/3] chore: fix lack of annotation --- packages/react-router/src/route.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-router/src/route.tsx b/packages/react-router/src/route.tsx index 01b2caf8517..34a935401de 100644 --- a/packages/react-router/src/route.tsx +++ b/packages/react-router/src/route.tsx @@ -255,7 +255,7 @@ export class Route< return useNavigate({ from: this.fullPath }) } - Link = React.forwardRef( + Link: LinkComponent<'a', TFullPath> = React.forwardRef( (props, ref: React.ForwardedRef) => { const router = useRouter() return ( @@ -453,7 +453,7 @@ export class RootRoute< return useNavigate({ from: this.fullPath }) } - Link = React.forwardRef( + Link: LinkComponent<'a', '/'> = React.forwardRef( (props, ref: React.ForwardedRef) => { const router = useRouter() return ( From 42d0c25c295e80623e135c90335fda17c011b7b8 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 6 May 2025 09:43:22 +0200 Subject: [PATCH 3/3] test: add routeApiTests for Link --- packages/react-router/tests/routeApi.test-d.tsx | 8 +++++++- packages/solid-router/tests/routeApi.test-d.tsx | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/react-router/tests/routeApi.test-d.tsx b/packages/react-router/tests/routeApi.test-d.tsx index 972be17b5e9..de7ff81ca36 100644 --- a/packages/react-router/tests/routeApi.test-d.tsx +++ b/packages/react-router/tests/routeApi.test-d.tsx @@ -1,6 +1,6 @@ import { describe, expectTypeOf, test } from 'vitest' import { createRootRoute, createRoute, createRouter, getRouteApi } from '../src' -import type { MakeRouteMatch, UseNavigateResult } from '../src' +import type { LinkComponent, MakeRouteMatch, UseNavigateResult } from '../src' const rootRoute = createRootRoute() @@ -87,6 +87,12 @@ describe('getRouteApi', () => { MakeRouteMatch >() }) + test('Link', () => { + const Link = invoiceRouteApi.Link + expectTypeOf(Link).toEqualTypeOf< + LinkComponent<'a', '/invoices/$invoiceId'> + >() + }) }) describe('createRoute', () => { diff --git a/packages/solid-router/tests/routeApi.test-d.tsx b/packages/solid-router/tests/routeApi.test-d.tsx index 99908a3d7a4..805eec55658 100644 --- a/packages/solid-router/tests/routeApi.test-d.tsx +++ b/packages/solid-router/tests/routeApi.test-d.tsx @@ -1,5 +1,6 @@ import { describe, expectTypeOf, test } from 'vitest' import { createRootRoute, createRoute, createRouter, getRouteApi } from '../src' +import type { LinkComponent } from '../src' import type { Accessor } from 'solid-js' import type { MakeRouteMatch, UseNavigateResult } from '@tanstack/router-core' @@ -96,6 +97,12 @@ describe('getRouteApi', () => { Accessor> >() }) + test('Link', () => { + const Link = invoiceRouteApi.Link + expectTypeOf(Link).toEqualTypeOf< + LinkComponent<'a', '/invoices/$invoiceId'> + >() + }) }) describe('createRoute', () => {