diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx index 125c5b20c7e..fcd60424f92 100644 --- a/packages/react-router/src/link.tsx +++ b/packages/react-router/src/link.tsx @@ -547,6 +547,13 @@ export interface LinkComponentRoute< ): React.ReactElement } +export interface CreateLinkOptions< + TRouter extends AnyRouter, + TFrom extends RoutePaths, +> { + from: TFrom +} + /** * Creates a typed Link-like component that preserves TanStack Router's * navigation semantics and type-safety while delegating rendering to the @@ -556,14 +563,28 @@ export interface LinkComponentRoute< * router-aware props (eg. `to`, `params`, `search`, `preload`). * * @param Comp The host component to render (eg. a design-system Link/Button) + * @param options Optional config with `from` for type-safe relative navigation * @returns A router-aware component with the same API as `Link`. + * @example + * const ButtonLink = createLink(MyButton) + * const DashboardLink = createLink(MyButton, { from: '/dashboard' }) + * // Type-safe relative to /dashboard * @link https://tanstack.com/router/latest/docs/framework/react/guide/custom-link */ -export function createLink( +export function createLink< + TRouter extends AnyRouter = RegisteredRouter, + const TComp = 'a', + const TFrom extends RoutePaths = RoutePaths< + TRouter['routeTree'] + >, +>( Comp: Constrain ReactNode>, -): LinkComponent { + options?: CreateLinkOptions, +): LinkComponent { return React.forwardRef(function CreatedLink(props, ref) { - return + return ( + + ) }) as any } diff --git a/packages/react-router/src/route.tsx b/packages/react-router/src/route.tsx index 3fb7716e7a5..830c53ec072 100644 --- a/packages/react-router/src/route.tsx +++ b/packages/react-router/src/route.tsx @@ -45,7 +45,9 @@ import type { UseLoaderDepsRoute } from './useLoaderDeps' import type { UseParamsRoute } from './useParams' import type { UseSearchRoute } from './useSearch' import type { UseRouteContextRoute } from './useRouteContext' -import type { LinkComponentRoute } from './link' +import type { LinkComponent, LinkComponentRoute, CreateLinkProps } from './link' +import type { Constrain } from '@tanstack/router-core' +import type { ReactNode } from 'react' declare module '@tanstack/router-core' { export interface UpdatableRouteOptionsExtensions { @@ -75,6 +77,9 @@ declare module '@tanstack/router-core' { useLoaderData: UseLoaderDataRoute useNavigate: () => UseNavigateResult Link: LinkComponentRoute + createLink: ( + Comp: Constrain ReactNode>, + ) => LinkComponent } } @@ -165,6 +170,17 @@ export class RouteApi< }) as unknown as LinkComponentRoute< RouteTypesById['fullPath'] > + + createLink( + Comp: Constrain ReactNode>, + ): LinkComponent['fullPath']> { + const id = this.id + return React.forwardRef(function CreatedLink(props: any, ref) { + const router = useRouter() + const from = router.routesById[id as string].fullPath + return + }) as any + } } export class Route< @@ -314,6 +330,15 @@ export class Route< return }, ) as unknown as LinkComponentRoute + + createLink( + Comp: Constrain ReactNode>, + ): LinkComponent { + const from = this.fullPath + return React.forwardRef(function CreatedLink(props: any, ref) { + return + }) as any + } } /** @@ -590,6 +615,15 @@ export class RootRoute< return }, ) as unknown as LinkComponentRoute<'/'> + + createLink( + Comp: Constrain ReactNode>, + ): LinkComponent { + const from = this.fullPath + return React.forwardRef(function CreatedLink(props: any, ref) { + return + }) as any + } } /** diff --git a/packages/react-router/tests/createLink-from.test-d.tsx b/packages/react-router/tests/createLink-from.test-d.tsx new file mode 100644 index 00000000000..8e5b344075e --- /dev/null +++ b/packages/react-router/tests/createLink-from.test-d.tsx @@ -0,0 +1,113 @@ +import { test } from 'vitest' +import React from 'react' +import { + createLink, + createRootRoute, + createRoute, + createRouter, +} from '../src' + +// Simple route tree: +// / +// /dashboard +// /dashboard/settings +// /dashboard/users +// /dashboard/users/$userId +// /posts + +const rootRoute = createRootRoute() + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/', +}) + +const dashboardRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'dashboard', +}) + +const settingsRoute = createRoute({ + getParentRoute: () => dashboardRoute, + path: 'settings', +}) + +const usersRoute = createRoute({ + getParentRoute: () => dashboardRoute, + path: 'users', +}) + +const userRoute = createRoute({ + getParentRoute: () => usersRoute, + path: '$userId', +}) + +const postsRoute = createRoute({ + getParentRoute: () => rootRoute, + path: 'posts', +}) + +const routeTree = rootRoute.addChildren([ + indexRoute, + dashboardRoute.addChildren([ + settingsRoute, + usersRoute.addChildren([userRoute]), + ]), + postsRoute, +]) + +const router = createRouter({ routeTree }) + +declare module '../src' { + interface Register { + router: typeof router + } +} + +// Custom component for createLink +const Button = (props: { children?: React.ReactNode }) => ( +