skilly. Buy ad slot
All skills
Frontend · React · Solidjs / AGENT SKILL

tanstack-router

tanstack-skills/tanstack-skills
4.4K installs 33 GitHub stars
0

Implement type-safe routing, search parameters, and data loading in React and Solid applications.
Type-safe routing for React and Solid applications with first-class search params, data loading, and seamless integration with the React ecosystem.

BEFORE YOU INSTALL

Understand the trade-offs.

SECURITY REVIEW

Not yet assessed

Review the original instructions and requested permissions before installing.

No security review is available for this catalog entry yet.

SKILL QUALITY

Not yet assessed

How clearly the skill guides your agent, how complete its workflow is, and how you can check the outcome.

No quality assessment is available for this catalog entry yet.

The full skill.

Original instructions from the publisher’s SKILL.md

## Overview

TanStack Router is a fully type-safe router for React (and Solid) applications. It provides file-based routing, first-class search parameter management, built-in data loading, code splitting, and deep TypeScript integration. It serves as the routing foundation for TanStack Start (the full-stack framework).

**Package:** `@tanstack/react-router`
**CLI:** `@tanstack/router-cli` or `@tanstack/router-plugin` (Vite/Rspack/Webpack)
**Devtools:** `@tanstack/react-router-devtools`

## Installation

```bash
npm install @tanstack/react-router
# For file-based routing with Vite:
npm install -D @tanstack/router-plugin
# Or standalone CLI:
npm install -D @tanstack/router-cli
```

## Core Concepts

### Route Trees

Routes are organized in a tree structure. The root route is the top-level layout, and child routes nest underneath.

```tsx
import { createRootRoute, createRoute, createRouter } from '@tanstack/react-router'

const rootRoute = createRootRoute({
  component: RootLayout,
})

const indexRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/',
  component: HomePage,
})

const aboutRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/about',
  component: AboutPage,
})

const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])
const router = createRouter({ routeTree })
```

### File-Based Routing

File-based routing automatically generates the route tree from your file structure. Configure with Vite plugin:

```ts
// vite.config.ts
import { defineConfig } from 'vite'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    TanStackRouterVite(),
    // ... other plugins
  ],
})
```

#### File Naming Conventions

| File Pattern | Route Type | Example Path |
|---|---|---|
| `__root.tsx` | Root layout | N/A (wraps all) |
| `index.tsx` | Index route | `/` |
| `about.tsx` | Static route | `/about` |
| `$postId.tsx` | Dynamic param | `/posts/$postId` |
| `posts.tsx` | Layout route | `/posts/*` (layout) |
| `posts/index.tsx` | Nested index | `/posts` |
| `posts/$postId.tsx` | Nested dynamic | `/posts/123` |
| `posts_.$postId.tsx` | Pathless layout | `/posts/123` (different layout) |
| `_layout.tsx` | Pathless layout | N/A (groups routes) |
| `_layout/dashboard.tsx` | Grouped route | `/dashboard` |
| `$.tsx` | Splat/catch-all | `/*` |
| `posts.$postId.edit.tsx` | Dot notation | `/posts/123/edit` |

#### Special Prefixes
- `_` prefix: Pathless routes (layout groups without URL segment)
- `$` prefix: Dynamic path parameters
- `(folder)` parentheses: Route groups (organizational, no URL impact)

### Route Configuration

Each route can define:

```tsx
// routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  // Validation for path params
  params: {
    parse: (params) => ({ postId: Number(params.postId) }),
    stringify: (params) => ({ postId: String(params.postId) }),
  },

  // Search params validation
  validateSearch: (search: Record<string, unknown>) => {
    return {
      page: Number(search.page ?? 1),
      filter: (search.filter as string) || '',
    }
  },

  // Data loading
  loader: async ({ params, context, abortController }) => {
    return fetchPost(params.postId)
  },

  // Loader dependencies (re-run loader when these change)
  loaderDeps: ({ search }) => ({ page: search.page }),

  // Stale time for cached loader data
  staleTime: 5_000,

  // Preloading
  preloadStaleTime: 30_000,

  // Error component
  errorComponent: PostErrorComponent,

  // Pending/loading component
  pendingComponent: PostLoadingComponent,

  // 404 component
  notFoundComponent: PostNotFoundComponent,

  // Before load hook (authentication, redirects)
  beforeLoad: async ({ context, location }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: '/login',
        search: { redirect: location.href },
      })
    }
  },

  // Head/meta management
  head: () => ({
    meta: [{ title: 'Post Details' }],
  }),

  // Component
  component: PostComponent,
})

function PostComponent() {
  const { postId } = Route.useParams()
  const post = Route.useLoaderData()
  const { page, filter } = Route.useSearch()

  return <div>{post.title}</div>
}
```

## Data Loading

### Route Loaders

```tsx
export const Route = createFileRoute('/posts')({
  loader: async ({ context }) => {
    // Access router context (e.g., queryClient)
    const posts = await context.queryClient.ensureQueryData({
      queryKey: ['posts'],
      queryFn: fetchPosts,
    })
    return { posts }
  },
  component: PostsComponent,
})

function PostsComponent() {
  const { posts } = Route.useLoaderData()
  // ...
}
```

### Loader Dependencies

Control when loaders re-execute:

```tsx
export const Route = createFileRoute('/posts')({
  loaderDeps: ({ search: { page, filter } }) => ({ page, filter }),
  loader: async ({ deps: { page, filter } }) => {
    return fetchPosts({ page, filter })
  },
})
```

### Deferred Data Loading

Stream non-critical data:

```tsx
import { Await, defer } from '@tanstack/react-router'

export const Route = createFileRoute('/dashboard')({
  loader: async () => {
    const criticalData = await fetchCriticalData()
    const deferredData = defer(fetchSlowData())
    return { criticalData, deferredData }
  },
  component: DashboardComponent,
})

function DashboardComponent() {
  const { criticalData, deferredData } = Route.useLoaderData()

  return (
    <div>
      <CriticalSection data={criticalData} />
      <Suspense fallback={<Loading />}>
        <Await promise={deferredData}>
          {(data) => <SlowSection data={data} />}
        </Await>
      </Suspense>
    </div>
  )
}
```

### Context-Based Data Loading

Provide shared dependencies via router context:

```tsx
// Create router with context
const router = createRouter({
  routeTree,
  context: {
    queryClient,
    auth: undefined!, // Will be provided by RouterProvider
  },
})

// In root/app component
function App() {
  const auth = useAuth()
  return <RouterProvider router={router} context={{ auth }} />
}

// In routes
export const Route = createFileRoute('/protected')({
  beforeLoad: ({ context }) => {
    if (!context.auth.user) throw redirect({ to: '/login' })
  },
  loader: ({ context }) => {
    return context.queryClient.ensureQueryData(userQueryOptions())
  },
})
```

## Search Parameters

### Validation

```tsx
import { z } from 'zod'

const postSearchSchema = z.object({
  page: z.number().default(1),
  filter: z.string().default(''),
  sort: z.enum(['date', 'title']).default('date'),
})

export const Route = createFileRoute('/posts')({
  validateSearch: postSearchSchema,
  // Or manual validation:
  // validateSearch: (search) => postSearchSchema.parse(search),
})
```

### Reading Search Params

```tsx
function PostsComponent() {
  // From route
  const { page, filter, sort } = Route.useSearch()

  // Or from any component with useSearch hook
  const search = useSearch({ from: '/posts' })
}
```

### Updating Search Params

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

function Pagination() {
  const navigate = useNavigate()
  const { page } = Route.useSearch()

  return (
    <button
      onClick={() =>
        navigate({
          search: (prev) => ({ ...prev, page: prev.page + 1 }),
        })
      }
    >
      Next Page
    </button>
  )
}

// Or via Link component
<Link
  to="/posts"
  search={(prev) => ({ ...prev, page: 2 })}
>
  Page 2
</Link>
```

### Search Param Options

```tsx
const router = createRouter({
  routeTree,
  // Custom serialization
  search: {
    strict: true, // Reject unknown params
  },
  // Default search param serializer
  stringifySearch: defaultStringifySearch,
  parseSearch: defaultParseSearch,
})
```

## Navigation

### Link Component

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

// Static route
<Link to="/about">About</Link>

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

// With search params
<Link to="/posts" search={{ page: 2, filter: 'react' }}>
  Page 2
</Link>

// Active link styling
<Link
  to="/posts"
  activeProps={{ className: 'active' }}
  inactiveProps={{ className: 'inactive' }}
  activeOptions={{ exact: true }}
>
  Posts
</Link>

// Preloading
<Link to="/posts" preload="intent">Posts</Link>
<Link to="/dashboard" preload="viewport">Dashboard</Link>

// Hash
<Link to="/docs" hash="api-reference">API Reference</Link>
```

### Programmatic Navigation

```tsx
import { useNavigate, useRouter } from '@tanstack/react-router'

function MyComponent() {
  const navigate = useNavigate()
  const router = useRouter()

  // Navigate to a route
  navigate({ to: '/posts', search: { page: 1 } })

  // Navigate with replace
  navigate({ to: '/posts', replace: true })

  // Relative navigation
  navigate({ to: '.', search: (prev) => ({ ...prev, page: 2 }) })

  // Go back/forward
  router.history.back()
  router.history.forward()

  // Invalidate and reload current route
  router.invalidate()
}
```

### Redirects

```tsx
import { redirect } from '@tanstack/react-router'

// In beforeLoad or loader
throw redirect({
  to: '/login',
  search: { redirect: location.href },
  // Optional status code
  statusCode: 301, // Permanent redirect (SSR)
})
```

### Navigation Blocking

```tsx
import { useBlocker } from '@tanstack/react-router'

function FormComponent() {
  const [isDirty, setIsDirty] = useState(false)

  useBlocker({
    shouldBlockFn: () => isDirty,
    withResolver: true, // Shows confirm dialog
  })

  // Or with custom UI
  const { proceed, reset, status } = useBlocker({
    shouldBlockFn: () => isDirty,
  })

  if (status === 'blocked') {
    return (
      <div>
        <p>Are you sure you want to leave?</p>
        <button onClick={proceed}>Leave</button>
        <button onClick={reset}>Stay</button>
      </div>
    )
  }
}
```

## Code Splitting

### Automatic (File-Based Routing)

With file-based routing, create a lazy file:

```
routes/
  posts.tsx          # Critical: loader, beforeLoad, meta
  posts.lazy.tsx     # Lazy: component, pendingComponent, errorComponent
```

```tsx
// posts.tsx (loaded eagerly)
export const Route = createFileRoute('/posts')({
  loader: () => fetchPosts(),
})

// posts.lazy.tsx (loaded lazily)
import { createLazyFileRoute } from '@tanstack/react-router'

export const Route = createLazyFileRoute('/posts')({
  component: PostsComponent,
  pendingComponent: PostsLoading,
  errorComponent: PostsError,
})
```

### Manual Code Splitting

```tsx
const postsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/posts',
  loader: () => fetchPosts(),
}).lazy(() => import('./posts.lazy').then((d) => d.Route))
```

## Preloading

```tsx
// Router-level defaults
const router = createRouter({
  routeTree,
  defaultPreload: 'intent', // 'intent' | 'viewport' | 'render' | false
  defaultPreloadStaleTime: 30_000, // 30 seconds
})

// Route-level
export const Route = createFileRoute('/posts/$postId')({
  // Stale time for the loader data
  staleTime: 5_000,
  // How long preloaded data stays fresh
  preloadStaleTime: 30_000,
})

// Link-level
<Link to="/posts" preload="intent" preloadDelay={100}>
  Posts
</Link>
```

## Type Safety

### Register Router Type

```tsx
// Declare module for type inference
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}
```

### Type-Safe Hooks

All hooks are fully typed based on the route tree:

```tsx
// useParams - typed to route's params
const { postId } = useParams({ from: '/posts/$postId' })

// useSearch - typed to route's search schema
const { page } = useSearch({ from: '/posts' })

// useLoaderData - typed to loader return
const data = useLoaderData({ from: '/posts/$postId' })

// useRouteContext - typed to route context
const { auth } = useRouteContext({ from: '/protected' })
```

### Route Generics

```tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  // TypeScript infers:
  // params: { postId: string }
  // search: validated search schema type
  // loaderData: return type of loader
  // context: router context type
})
```

## Authenticated Routes

```tsx
// __root.tsx
export const Route = createRootRouteWithContext<{
  auth: AuthContext
}>()({
  component: RootComponent,
})

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

// _authenticated/dashboard.tsx
export const Route = createFileRoute('/_authenticated/dashboard')({
  component: Dashboard, // Only accessible when authenticated
})
```

## Scroll Restoration

```tsx
const router = createRouter({
  routeTree,
  // Enable scroll restoration
  defaultScrollRestoration: true,
})

// Or per-route
export const Route = createFileRoute('/posts')({
  // Scroll to top on navigation
  scrollRestoration: true,
})

// Custom scroll restoration key
<ScrollRestoration
  getKey={(location) => location.pathname}
/>
```

## Route Masking

Display a different URL than the actual route:

```tsx
<Link
  to="/photos/$photoId"
  params={{ photoId: photo.id }}
  mask={{ to: '/photos', search: { photoId: photo.id } }}
>
  View Photo
</Link>

// Or programmatically
navigate({
  to: '/photos/$photoId',
  params: { photoId: photo.id },
  mask: { to: '/photos', search: { photoId: photo.id } },
})
```

## Not Found Handling

```tsx
// Global 404
const router = createRouter({
  routeTree,
  defaultNotFoundComponent: () => <div>Page not found</div>,
})

// Route-level 404
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    if (!post) throw notFound()
    return post
  },
  notFoundComponent: () => <div>Post not found</div>,
})
```

## Head Management

```tsx
export const Route = createFileRoute('/posts/$postId')({
  head: ({ loaderData }) => ({
    meta: [
      { title: loaderData.title },
      { name: 'description', content: loaderData.excerpt },
      { property: 'og:title', content: loaderData.title },
    ],
    links: [
      { rel: 'canonical', href: `https://example.com/posts/${loaderData.id}` },
    ],
  }),
})
```

## Integration with TanStack Query

```tsx
import { queryOptions } from '@tanstack/react-query'

const postsQueryOptions = queryOptions({
  queryKey: ['posts'],
  queryFn: fetchPosts,
})

export const Route = createFileRoute('/posts')({
  loader: ({ context: { queryClient } }) => {
    // Ensure data is in cache, won't refetch if fresh
    return queryClient.ensureQueryData(postsQueryOptions)
  },
  component: PostsComponent,
})

function PostsComponent() {
  // Use the same query options for reactive updates
  const { data: posts } = useSuspenseQuery(postsQueryOptions)
  return <PostsList posts={posts} />
}
```

## Router Hooks Reference

| Hook | Purpose |
|------|---------|
| `useRouter()` | Access router instance |
| `useRouterState()` | Subscribe to router state |
| `useParams()` | Get route path params |
| `useSearch()` | Get validated search params |
| `useLoaderData()` | Get route loader data |
| `useRouteContext()` | Get route context |
| `useNavigate()` | Get navigate function |
| `useLocation()` | Get current location |
| `useMatches()` | Get all matched routes |
| `useMatch()` | Get specific route match |
| `useBlocker()` | Block navigation |
| `useLinkProps()` | Get link props for custom components |
| `useMatchRoute()` | Check if a route matches |

## Best Practices

1. **Use file-based routing** for most applications - it's simpler and auto-generates the route tree
2. **Validate search params** with Zod or custom validators for type safety
3. **Use `loaderDeps`** to control when loaders re-execute based on search param changes
4. **Leverage context** for dependency injection (QueryClient, auth state)
5. **Use `beforeLoad`** for authentication guards, not in components
6. **Separate critical vs lazy code** - keep loaders in the main file, components in `.lazy.tsx`
7. **Use `preload="intent"`** on Links for perceived performance
8. **Use `staleTime`** to prevent unnecessary refetches during navigation
9. **Register the router type** for full TypeScript inference across the app
10. **Use `notFound()`** instead of conditional rendering for 404 states
11. **Colocate search param logic** with routes that own them
12. **Use pathless layouts** (`_authenticated`) for shared auth/layout logic without URL segments

## Common Pitfalls

- Forgetting to register the router type (`declare module`)
- Not using `loaderDeps` when loader depends on search params (causes stale data)
- Putting auth checks in components instead of `beforeLoad` (flash of protected content)
- Not handling the loading state with `pendingComponent`
- Using `useEffect` for data fetching instead of route loaders
- Mutating search params directly instead of using navigate/Link
- Not wrapping the app with `RouterProvider`
- Forgetting `getParentRoute` in code-based route definitions

Skill folder

Files included alongside SKILL.md in the publisher’s repository.