tanstack-router

star 0

Build type-safe routing for React apps using TanStack Router v1 and TanStack Start. Covers file-based routing, search params, data loading, authenticated routes, code splitting, navigation, server functions, and Vite setup. Use when the user works with TanStack Router, TanStack Start, file-based routes, createFileRoute, or asks about type-safe routing in React.

5amfung By 5amfung schedule Updated 2/27/2026

name: tanstack-router description: Build type-safe routing for React apps using TanStack Router v1 and TanStack Start. Covers file-based routing, search params, data loading, authenticated routes, code splitting, navigation, server functions, and Vite setup. Use when the user works with TanStack Router, TanStack Start, file-based routes, createFileRoute, or asks about type-safe routing in React.

TanStack Router v1

Type-safe router for React with built-in caching, URL state management, and file-based routing.

  • Package: @tanstack/react-router (v1.x)
  • Full-stack: @tanstack/react-start (RC, built on TanStack Router)
  • Requires: React 18+, TypeScript 5.3+

Quick Start

New Project (SPA)

npx create-tsrouter-app@latest my-app --template file-router
cd my-app && npm run dev

New Project (Full-Stack with TanStack Start)

npm create @tanstack/start@latest

Add to Existing Project

bun install @tanstack/react-router
bun install -D @tanstack/router-plugin

Vite Setup (File-Based Routing)

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    tanstackRouter({
      target: 'react',
      autoCodeSplitting: true,
    }),
    react(),
  ],
})

Default config: routes in ./src/routes, generates ./src/routeTree.gen.ts.

File Naming Conventions

Convention Purpose
__root.tsx Root route (always rendered)
index.tsx Exact match for parent path
$param.tsx Dynamic path parameter segment
_layout.tsx Pathless layout (prefix _ = no URL segment)
route_.child.tsx Suffix _ = escape parent nesting
. separator Flat route nesting: posts.index.tsx = /posts (exact)
(group) folder Route group (not in URL path)
-prefix Excluded from route tree (colocation)
route.tsx Directory route file: posts/route.tsx = /posts
.lazy.tsx Code-split non-critical route config

File-Based Routing Examples

Directory style:

src/routes/
  __root.tsx          → <Root>
  index.tsx           → / (exact)
  about.tsx           → /about
  posts.tsx           → /posts (layout)
  posts/
    index.tsx         → /posts (exact)
    $postId.tsx       → /posts/$postId
  _auth.tsx           → pathless layout
  _auth/
    dashboard.tsx     → /dashboard (wrapped by _auth layout)

Flat style:

src/routes/
  __root.tsx
  posts.tsx
  posts.index.tsx        → /posts (exact)
  posts.$postId.tsx      → /posts/$postId
  settings.profile.tsx   → /settings/profile

Creating Routes

// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router'

export const Route = createRootRoute({
  component: () => (
    <div>
      <nav>{/* links */}</nav>
      <Outlet />
    </div>
  ),
})
// src/routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
  component: PostsLayout,
})

function PostsLayout() {
  return <div><Outlet /></div>
}

Navigation

Link Component

import { Link } from '@tanstack/react-router'

// Static link.
<Link to="/about">About</Link>

// Dynamic params.
<Link to="/posts/$postId" params={{ postId: '123' }}>Post</Link>

// Search params.
<Link to="/posts" search={{ page: 1, sort: 'newest' }}>Posts</Link>

// Update search params functionally.
<Link to="." search={(prev) => ({ ...prev, page: prev.page + 1 })}>
  Next Page
</Link>

// Active styling.
<Link
  to="/about"
  activeProps={{ className: 'font-bold' }}
  activeOptions={{ exact: true }}
>
  About
</Link>

// Preloading on hover.
<Link to="/posts" preload="intent">Posts</Link>

Imperative Navigation

import { useNavigate } from '@tanstack/react-router'

function Component() {
  const navigate = useNavigate({ from: '/posts/$postId' })

  const handleClick = () => {
    navigate({ to: '/posts', search: { page: 1 } })
  }
}

Search Params (Type-Safe URL State)

TanStack Router parses search params as JSON, preserving types (numbers, booleans, arrays, objects).

Validation with Zod (Recommended)

import { createFileRoute } from '@tanstack/react-router'
import { zodValidator, fallback } from '@tanstack/zod-adapter'
import { z } from 'zod'

const searchSchema = z.object({
  page: fallback(z.number(), 1).default(1),
  filter: fallback(z.string(), '').default(''),
  sort: fallback(z.enum(['newest', 'oldest', 'price']), 'newest').default('newest'),
})

export const Route = createFileRoute('/products')({
  validateSearch: zodValidator(searchSchema),
})

Use fallback() from @tanstack/zod-adapter instead of .catch() to retain types.

Reading Search Params

// In route component.
const { page, filter, sort } = Route.useSearch()

// From another file (avoids circular imports).
import { getRouteApi } from '@tanstack/react-router'
const routeApi = getRouteApi('/products')
const search = routeApi.useSearch()

Search Middlewares

import { retainSearchParams, stripSearchParams } from '@tanstack/react-router'

export const Route = createFileRoute('/products')({
  validateSearch: zodValidator(searchSchema),
  search: {
    middlewares: [
      retainSearchParams(['rootValue']),
      stripSearchParams({ sort: 'newest' }),
    ],
  },
})

Data Loading

Route Loaders

export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
})

// Consume in component.
function Posts() {
  const posts = Route.useLoaderData()
  return <div>{posts.map(p => <p key={p.id}>{p.title}</p>)}</div>
}

Loader with Search Param Dependencies

Use loaderDeps to key cache on specific search params. Only include deps you actually use.

export const Route = createFileRoute('/posts')({
  validateSearch: z.object({ offset: z.number().catch(0) }),
  loaderDeps: ({ search: { offset } }) => ({ offset }),
  loader: ({ deps: { offset } }) => fetchPosts({ offset }),
})

Loader with Path Params

export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params: { postId } }) => fetchPostById(postId),
})

Router Context (Dependency Injection)

// __root.tsx
export const Route = createRootRouteWithContext<{
  queryClient: QueryClient
}>()({
  component: RootComponent,
})

// posts.tsx - access context in loader.
export const Route = createFileRoute('/posts')({
  loader: ({ context: { queryClient } }) =>
    queryClient.ensureQueryData(postsQueryOptions()),
})

// router.tsx
const router = createRouter({
  routeTree,
  context: { queryClient },
})

Caching Defaults

  • staleTime: 0 (always refetch on navigation)
  • preloadStaleTime: 30s
  • gcTime: 30min
  • router.invalidate() forces all loaders to refetch.

Authenticated Routes

Use beforeLoad to guard routes. Throw redirect() to send unauthenticated users to login.

// src/routes/_authenticated.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated')({
  beforeLoad: async ({ location }) => {
    if (!isAuthenticated()) {
      throw redirect({
        to: '/login',
        search: { redirect: location.href },
      })
    }
  },
})

With React Context

// __root.tsx
export const Route = createRootRouteWithContext<{ auth: AuthState }>()({
  component: () => <Outlet />,
})

// App.tsx
function InnerApp() {
  const auth = useAuth()
  return <RouterProvider router={router} context={{ auth }} />
}

// _authenticated.tsx
export const Route = createFileRoute('/_authenticated')({
  beforeLoad: ({ context, location }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({ to: '/login', search: { redirect: location.href } })
    }
  },
})

Code Splitting

With autoCodeSplitting: true in Vite config, route components are automatically split.

Manual splitting uses .lazy.tsx:

// posts.tsx (critical: loader, validateSearch)
export const Route = createFileRoute('/posts')({
  loader: fetchPosts,
})

// posts.lazy.tsx (non-critical: component)
import { createLazyFileRoute } from '@tanstack/react-router'

export const Route = createLazyFileRoute('/posts')({
  component: Posts,
})

function Posts() { /* ... */ }

Error Handling

export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
  errorComponent: ({ error, reset }) => (
    <div>
      <p>{error.message}</p>
      <button onClick={() => router.invalidate()}>Retry</button>
    </div>
  ),
  pendingComponent: () => <div>Loading...</div>,
  notFoundComponent: () => <div>Not found</div>,
})

Route Masking

Show a different URL in the browser bar than the one actually navigated to — useful for modals that have their own URL.

// Navigate to a modal route but display a "clean" URL.
navigate({
  to: '/photos/$photoId',
  params: { photoId: photo.id },
  mask: {
    to: '/photos',
  },
})

// Or via Link.
<Link
  to="/photos/$photoId"
  params={{ photoId: photo.id }}
  mask={{ to: '/photos' }}
>
  View Photo
</Link>

When the masked URL is shared or the page is reloaded, the browser navigates to the displayed (masked) URL, not the underlying route.

Scroll Restoration

TanStack Router handles scroll restoration automatically. Enable via router config:

const router = createRouter({
  routeTree,
  scrollRestoration: true, // Restore scroll on back/forward navigation.
})

For custom scrollable containers (e.g., virtualized lists), use useElementScrollRestoration:

import { useElementScrollRestoration } from '@tanstack/react-router'

function VirtualList({ id }: { id: string }) {
  const scrollEntry = useElementScrollRestoration({ id })

  const virtualizer = useVirtualizer({
    // Use saved scroll position as initial offset.
    initialOffset: scrollEntry?.scrollY ?? 0,
  })
  // ...
}

To prevent scroll reset on a specific navigation:

<Link to="/posts" resetScroll={false}>Posts (no scroll reset)</Link>

TanStack Start (Full-Stack)

TanStack Start extends Router with SSR, streaming, server functions, and API routes. See reference.md for server function patterns.

Server Functions

import { createServerFn } from '@tanstack/react-start'

export const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
  return db.query.posts.findMany()
})

// Use in loader.
export const Route = createFileRoute('/posts')({
  loader: () => getPosts(),
})

Server Function with Validation

import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'

export const createPost = createServerFn({ method: 'POST' })
  .inputValidator(z.object({ title: z.string().min(1), body: z.string() }))
  .handler(async ({ data }) => {
    return db.insert(posts).values(data).returning()
  })

Additional Resources

Install via CLI
npx skills add https://github.com/5amfung/appfolio-mock --skill tanstack-router
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator