From 0f3b09cc8be1a79999872dd7642643ecbeca1ccb Mon Sep 17 00:00:00 2001 From: imsherr Date: Sun, 11 Jan 2026 19:16:43 -0500 Subject: [PATCH 1/3] Add `from` option to `createLink` for type-safe relative navigation Adds optional `{ from }` config to `createLink` for fixing the origin path. Enables type-safe relative navigation (e.g., `./child`, `../sibling`) from a specific route. --- packages/react-router/src/link.tsx | 27 ++++- .../tests/createLink-from.test-d.tsx | 108 ++++++++++++++++++ 2 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 packages/react-router/tests/createLink-from.test-d.tsx 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/tests/createLink-from.test-d.tsx b/packages/react-router/tests/createLink-from.test-d.tsx new file mode 100644 index 00000000000..7694f40e899 --- /dev/null +++ b/packages/react-router/tests/createLink-from.test-d.tsx @@ -0,0 +1,108 @@ +import { expectTypeOf, 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 }) => ( +