Gojiberry AI
AI agents that find and contact high-intent leads for you
Try Gojiberry free →
Hermes Agent
Run your Hermes agent, fully managed
Launch on Hostinger →
Hostinger VPS
Spin up a VPS in one click, 20% off
Launch on Hostinger →
Firecrawl
Crawl and scrape any site into clean data
Try Firecrawl free →
Runable
One AI agent to build, run, and grow your business
Try Runable free →
Context.dev
One API to scrape, enrich, and extract the web
Start building free →
Jotform
Forms, workflows, and AI Agents for your team
Try Jotform free →
Runable
One AI agent to build, run, and grow your business
Try Runable free →
OpenClaw
Deploy a managed OpenClaw agent in 60 seconds
Launch on Hostinger →
Sponsor here
9/10 sponsor slots taken — 1 left
Claim it →
Claude Market
Menu
SkillsMCPPluginsMarketplacesNewsletterSubmit MCPSkillPluginMCPMCP, plugin, or skillAdvertise
Claude Market
SkillsMCPPluginsMarketplacesNewsletterSubmit MCPSkillPluginMCPMCP, plugin, or skillAdvertise
Skills/jezweb/claude-skills/tanstack-router
tanstack-router logo

tanstack-router

jezweb/claude-skills
548 installs
Run it on Hostinger, 20% off →Your friend gets 20% off too, using this linkFree API →|Prompt Injection|View on GitHub|Create your own skill →

Installation

npx skills add https://github.com/jezweb/claude-skills --skill tanstack-router

Summary

|

SKILL.md

TanStack Router

Type-safe, file-based routing for React SPAs with route-level data loading and TanStack Query integration

---

Quick Start

Last Updated: 2026-01-09 Version: @tanstack/react-router@1.146.2

npm install @tanstack/react-router @tanstack/router-devtools
npm install -D @tanstack/router-plugin
# Optional: Zod validation adapter
npm install @tanstack/zod-adapter zod

Vite Config (TanStackRouterVite MUST come before react()):

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

export default defineConfig({
  plugins: [TanStackRouterVite(), react()], // Order matters!
})

File Structure:

src/routes/
├── __root.tsx         → createRootRoute() with <Outlet />
├── index.tsx          → createFileRoute('/')
└── posts.$postId.tsx  → createFileRoute('/posts/$postId')

App Setup:

import { createRouter, RouterProvider } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen' // Auto-generated by plugin

const router = createRouter({ routeTree })
<RouterProvider router={router} />

---

Core Patterns

Type-Safe Navigation (routes auto-complete, params typed):

<Link to="/posts/$postId" params={{ postId: '123' }} />
<Link to="/invalid" /> // ❌ TypeScript error

Route Loaders (data fetching before render):

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => ({ post: await fetchPost(params.postId) }),
  component: ({ useLoaderData }) => {
    const { post } = useLoaderData() // Fully typed!
    return <h1>{post.title}</h1>
  },
})

TanStack Query Integration (prefetch + cache):

const postOpts = (id: string) => queryOptions({
  queryKey: ['posts', id],
  queryFn: () => fetchPost(id),
})

export const Route = createFileRoute('/posts/$postId')({
  loader: ({ context: { queryClient }, params }) =>
    queryClient.ensureQueryData(postOpts(params.postId)),
  component: () => {
    const { postId } = Route.useParams()
    const { data } = useQuery(postOpts(postId))
    return <h1>{data.title}</h1>
  },
})

---

Virtual File Routes (v1.140+)

Programmatic route configuration when file-based conventions don't fit your needs:

Install: npm install @tanstack/virtual-file-routes

Vite Config:

import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    tanstackRouter({
      target: 'react',
      virtualRouteConfig: './routes.ts', // Point to your routes file
    }),
    react(),
  ],
})

routes.ts (define routes programmatically):

import { rootRoute, route, index, layout, physical } from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  index('home.tsx'),
  route('/posts', 'posts/posts.tsx', [
    index('posts/posts-home.tsx'),
    route('$postId', 'posts/posts-detail.tsx'),
  ]),
  layout('first', 'layout/first-layout.tsx', [
    route('/nested', 'nested.tsx'),
  ]),
  physical('/classic', 'file-based-subtree'), // Mix with file-based
])

Use Cases: Custom route organization, mixing file-based and code-based, complex nested layouts.

---

Search Params Validation (Zod Adapter)

Type-safe URL search params with runtime validation:

Basic Pattern (inline validation):

import { z } from 'zod'

export const Route = createFileRoute('/products')({
  validateSearch: (search) => z.object({
    page: z.number().catch(1),
    filter: z.string().catch(''),
    sort: z.enum(['newest', 'oldest', 'price']).catch('newest'),
  }).parse(search),
})

Recommended Pattern (Zod adapter with fallbacks):

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

const searchSchema = z.object({
  query: z.string().min(1).max(100),
  page: fallback(z.number().int().positive(), 1),
  sortBy: z.enum(['name', 'date', 'relevance']).optional(),
})

export const Route = createFileRoute('/search')({
  validateSearch: zodValidator(searchSchema),
  // Type-safe: Route.useSearch() returns typed params
})

Why .catch() over .default(): Use .catch() to silently fix malformed params. Use .default() + errorComponent to show validation errors.

---

Error Boundaries

Handle errors at route level with typed error components:

Route-Level Error Handling:

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    if (!post) throw new Error('Post not found')
    return { post }
  },
  errorComponent: ({ error, reset }) => (
    <div>
      <p>Error: {error.message}</p>
      <button onClick={reset}>Retry</button>
    </div>
  ),
})

Default Error Component (global fallback):

const router = createRouter({
  routeTree,
  defaultErrorComponent: ({ error }) => (
    <div className="error-page">
      <h1>Something went wrong</h1>
      <p>{error.message}</p>
    </div>
  ),
})

Not Found Handling:

export const Route = createFileRoute('/posts/$postId')({
  notFoundComponent: () => <div>Post not found</div>,
})

---

Authentication with beforeLoad

Protect routes before they load (no flash of protected content):

Single Route Protection:

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

export const Route = createFileRoute('/dashboard')({
  beforeLoad: async ({ context }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: '/login',
        search: { redirect: location.pathname }, // Save for post-login
      })
    }
  },
})

Protect Multiple Routes (layout route pattern):

// routes/(authenticated)/route.tsx - protects all children
export const Route = createFileRoute('/(authenticated)')({
  beforeLoad: async ({ context }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({ to: '/login' })
    }
  },
})

Passing Auth Context (from React hooks):

// main.tsx - pass auth state to router
function App() {
  const auth = useAuth() // Your auth hook

  return (
    <RouterProvider
      router={router}
      context={{ auth }} // Available in beforeLoad
    />
  )
}

---

Known Issues Prevention

This skill prevents 20 documented issues:

Issue #1: Devtools Dependency Resolution

  • Error: Build fails with @tanstack/router-devtools-core not found
  • Fix: npm install @tanstack/router-devtools

Issue #2: Vite Plugin Order (CRITICAL)

  • Error: Routes not auto-generated, routeTree.gen.ts missing
  • Fix: TanStackRouterVite MUST come before react() in plugins array
  • Why: Plugin processes route files before React compilation

Issue #3: Type Registration Missing

  • Error: <Link to="..."> not typed, no autocomplete
  • Fix: Import routeTree from ./routeTree.gen in main.tsx to register types

Issue #4: Loader Not Running

  • Error: Loader function not called on navigation
  • Fix: Ensure route exports Route constant: export const Route = createFileRoute('/path')({ loader: ... })

Issue #5: Memory Leak with TanStack Form (FIXED)

  • Error: Production crashes when using TanStack Form + Router
  • Source: GitHub Issue #5734 (closed Jan 5, 2026)
  • Resolution: Fixed in latest versions of @tanstack/form and @tanstack/react-start. Update both packages to resolve.

Issue #6: Virtual Routes Index/Layout Conflict

  • Error: route.tsx and index.tsx conflict when using physical() in virtual routing
  • Source: GitHub Issue #5421
  • Fix: Use pathless route instead: _layout.tsx + _layout.index.tsx

Issue #7: Search Params Type Inference

  • Error: Type inference not working with zodSearchValidator
  • Source: GitHub Issue #3100 (regression since v1.81.5)
  • Fix: Use zodValidator from @tanstack/zod-adapter instead

Issue #8: TanStack Start Validators on Reload

  • Error: validateSearch not working on page reload in TanStack Start
  • Source: GitHub Issue #3711
  • Note: Works on client-side navigation, fails on direct page load

Issue #9: Server Function Validation Errors Lose Structure

Error: inputValidator Zod errors stringified, losing structure on client Source: GitHub Issue #6428 Why It Happens: TanStack Start server function error serialization converts Zod issues array to JSON string in error.message, making it unusable without manual parsing.

Prevention:

// Server function with input validation
export const myFn = createServerFn({ method: 'POST' })
  .inputValidator(z.object({
    name: z.string().min(2),
    age: z.number().min(18),
  }))
  .handler(async ({ data }) => data)

// Client: Workaround to parse stringified issues
try {
  await mutation.mutate({ data: invalidData })
} catch (error) {
  if (error.message.startsWith('[')) {
    const issues = JSON.parse(error.message)
    // Now can use structured error data
    issues.forEach(issue => {
      console.log(issue.path, issue.message)
    })
  }
}

Official Status: Known issue, tracking PR for fix

Issue #10: useParams({ strict: false }) Returns Unparsed Values

Error: Params typed as parsed but returned as strings after navigation Source: GitHub Issue #6385 Why It Happens: In v1.147.3+, match.params is no longer parsed when using strict: false. First render works correctly, but after navigation values are stored as strings instead of parsed types.

Prevention:

// Route with param parsing
export const Route = createFileRoute('/posts/$postId')({
  params: {
    parse: (params) => ({
      postId: z.coerce.number().parse(params.postId),
    }),
  },
})

// Component: Use strict mode (default) for parsed params
function Component() {
  const { postId } = useParams() // ✓ Parsed as number
  // const { postId } = useParams({ strict: false }) // ✗ String!

  // Or manually parse when using strict: false
  const params = useParams({ strict: false })
  const postId = Number(params.postId)
}

Official Status: Known issue, workaround required

Issue #11: Pathless Route notFoundComponent Not Rendering

Error: notFoundComponent on pathless layout routes ignored Source: GitHub Issue #6351, GitHub Issue #4065 Why It Happens: Pathless routes (e.g., routes/(authenticated)/route.tsx) don't render their notFoundComponent. Instead, the defaultNotFoundComponent from router config is triggered. This has been broken since April 2025.

Prevention:

// ✗ Doesn't work: notFoundComponent on pathless layout
export const Route = createFileRoute('/(authenticated)')({
  beforeLoad: ({ context }) => {
    if (!context.auth) throw redirect({ to: '/login' })
  },
  notFoundComponent: () => <div>Protected 404</div>, // Not rendered!
})

// ✓ Works: Define on child routes instead
export const Route = createFileRoute('/(authenticated)/dashboard')({
  notFoundComponent: () => <div>Protected 404</div>,
})

Official Status: Known issue, workaround required

Issue #12: Aborted Loader Renders errorComponent with Undefined Error

Error: Rapid navigation aborts previous loader and renders errorComponent with undefined error Source: GitHub Issue #6388 Why It Happens: Side effect introduced after PR #4570. When user rapidly navigates (e.g., clicking through list items), aborted fetch requests trigger errorComponent without passing the abort error.

Prevention:

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params, abortController }) => {
    await fetch(`/api/posts/${params.postId}`, {
      signal: abortController.signal,
    })
  },
  errorComponent: ({ error, reset }) => {
    // Check for undefined error (aborted request)
    if (!error) {
      return null // Or show loading state
    }
    return <div>Error: {error.message}</div>
  },
})

Official Status: Known issue, workaround required

Issue #13: Vitest Cannot Read Properties of Null (useState)

Error: Cannot read properties of null (reading 'useState') when running tests with Vitest Source: GitHub Issue #6262, PR #6074 Why It Happens: TanStack Start's tanstackStart() plugin conflicts with Vitest's React hooks rendering. This is a known duplicate issue with a PR in progress.

Prevention:

// Temporary workaround: Comment out tanstackStart() for tests
// vite.config.ts
export default defineConfig({
  plugins: [
    // tanstackStart(), // Disable for tests
    react(),
  ],
  test: { environment: 'jsdom' },
})

Official Status: PR #6074 in progress to fix

Issue #14: Throwing Error in Streaming SSR Loader Crashes Dev Server

Error: Dev server crashes when route loader throws error without awaiting (using void instead of await) Source: GitHub Issue #6200 Why It Happens: SSR streaming mode can't handle unawaited promise rejections. The error escapes the loader context and crashes the worker process.

Prevention:

// ✗ Wrong: void + throw crashes dev server
export const Route = createFileRoute('/posts')({
  loader: async () => {
    void fetch('/api/posts').then(r => {
      throw new Error('boom') // Crashes!
    })
  },
})

// ✓ Correct: Always await or catch
export const Route = createFileRoute('/posts')({
  loader: async () => {
    try {
      const data = await fetch('/api/posts')
      return data
    } catch (error) {
      throw error // Caught by errorComponent
    }
  },
})

Official Status: Known issue, workaround required

Issue #15: Prerender Hangs Indefinitely if Filter Returns Zero Results

Error: Build step hangs when prerender.filter returns zero routes Source: GitHub Issue #6425 Why It Happens: TanStack Start prerendering doesn't handle empty route sets gracefully - it waits indefinitely for routes that never come.

Prevention:

// ✗ Wrong: Empty filter causes hang
tanstackStart({
  prerender: {
    enabled: true,
    filter: (route) => false, // No routes → hangs!
  },
})

// ✓ Correct: Ensure at least one route or disable
tanstackStart({
  prerender: {
    enabled: true,
    filter: (route) => route.path === '/' || route.path.startsWith('/posts'),
  },
})

// Or temporarily disable
tanstackStart({
  prerender: { enabled: false },
})

Official Status: Known issue, workaround required

Issue #16: Prerendering Does Not Work in Docker

Error: Build fails in Docker with "Unable to connect" during prerender step Source: GitHub Issue #6275, PR #6305 Why It Happens: Vite preview server used for prerendering is not accessible in Docker environment.

Prevention:

// vite.config.ts - Make preview server accessible in Docker
export default defineConfig({
  preview: {
    host: true, // Bind to 0.0.0.0 instead of localhost
  },
  plugins: [
    devtools(),
    // nitro({ preset: "bun" }), // Remove temporarily if issues persist
    tanstackStart(),
    react(),
  ],
})

Official Status: PR #6305 in progress

Issue #17: Route Head Function Executes Before Loader Finishes

Error: Meta tags generated with incomplete data when head() runs before loader() Source: GitHub Issue #6221 Why It Happens: The head() function can execute before the route loader() finishes, causing meta tags to use placeholder or undefined data.

Prevention:

// ✗ Wrong: loaderData may not be available yet
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    return { post }
  },
  head: ({ loaderData }) => ({
    meta: [
      { title: loaderData.post.title }, // May be undefined!
    ],
  }),
})

// ✓ Correct: Explicitly await if needed
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    return { post }
  },
  head: async ({ loaderData }) => {
    await loaderData // Ensure loaded
    return {
      meta: [{ title: loaderData.post.title }],
    }
  },
})

Official Status: Known issue, workaround required

Issue #18: Virtual Routes Don't Support Manual Lazy Loading (Community-sourced)

Error: createLazyFileRoute automatically replaced with createFileRoute in virtual routes Source: GitHub Issue #6396 Why It Happens: Virtual file routes are designed for automatic code splitting only. Manual lazy routes are not supported - the plugin silently replaces them.

Prevention:

// Virtual routes: Use automatic code splitting
// vite.config.ts
tanstackRouter({
  target: 'react',
  virtualRouteConfig: './routes.ts',
  autoCodeSplitting: true, // Use automatic splitting
})

// Don't use createLazyFileRoute in virtual routes
// It will be replaced with createFileRoute automatically

Official Status: By design (documented behavior)

Issue #19: NavigateOptions Type Safety Inconsistency (Community-sourced)

Error: NavigateOptions type doesn't enforce required params like useNavigate() does Source: TkDodo's Blog: The Beauty of TanStack Router Why It Happens: Type definitions differ between runtime hook and type helper. NavigateOptions is less strict.

Prevention:

// ✗ Wrong: NavigateOptions doesn't catch missing params
const options: NavigateOptions = {
  to: '/posts/$postId', // No TS error, but params required!
}

// ✓ Correct: Use useNavigate() return type
const navigate = useNavigate()
type NavigateFn = typeof navigate
// Now type-safe across all usages

Verified: Cross-referenced with TanStack Query maintainer analysis

Issue #20: Missing Leading Slash in Route Paths (Community-sourced)

Error: Routes fail to match when path defined without leading slash Source: Official Debugging Guide Why It Happens: Very common beginner mistake - using 'about' instead of '/about' causes route matching failures.

Prevention:

// ✗ Wrong: Missing leading slash
export const Route = createFileRoute('about')({ /* ... */ })

// ✓ Correct: Always start with /
export const Route = createFileRoute('/about')({ /* ... */ })

Verified: Official documentation, common debugging issue

---

Cloudflare Workers Integration

Vite Config (add @cloudflare/vite-plugin):

import { cloudflare } from '@cloudflare/vite-plugin'

export default defineConfig({
  plugins: [TanStackRouterVite(), react(), cloudflare()],
})

API Routes Pattern (fetch from Workers backend):

// Worker: functions/api/posts.ts
export async function onRequestGet({ env }) {
  const { results } = await env.DB.prepare('SELECT * FROM posts').all()
  return Response.json(results)
}

// Router: src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: async () => fetch('/api/posts').then(r => r.json()),
})

---

Related Skills: tanstack-query (data fetching), react-hook-form-zod (form validation), cloudflare-worker-base (API backend), tailwind-v4-shadcn (UI)

Related Packages: @tanstack/zod-adapter (search validation), @tanstack/virtual-file-routes (programmatic routes)

---

Last verified: 2026-01-20 | Skill version: 2.0.0 | Changes: Added 12 new issues from community research (inputValidator structure loss, useParams parsing bug, pathless notFoundComponent, aborted loader errors, Vitest conflicts, SSR streaming crashes, Docker prerender issues, head/loader timing, virtual routes lazy loading limitation, NavigateOptions type inconsistency, leading slash common mistake). Increased error prevention from 8 to 20 documented issues.

Score

0–100
55/ 100

Grade

C

Popularity15/30

548 installs — growing adoption.

Completeness19/30

Documented: full SKILL.md body, one-line install. Missing: description, category/license metadata.

Trust15/25

Community skill with a public GitHub source repository you can review.

Freshness6/15

No update timestamp is tracked for this skill in our catalog.

Scored automatically from popularity, completeness, trust, and freshness — computed only from data in our catalog, never fabricated.

Proud of your score? Add this badge to your README.

Paste a snippet into your GitHub README. The badge updates automatically and links back to this page.

Tanstack Router skill score badge previewScore badge

Markdown

[![Tanstack Router skill](https://www.claudemarket.ai/skills/jezweb/claude-skills/tanstack-router/badges/score.svg)](https://www.claudemarket.ai/skills/jezweb/claude-skills/tanstack-router)

HTML

<a href="https://www.claudemarket.ai/skills/jezweb/claude-skills/tanstack-router"><img src="https://www.claudemarket.ai/skills/jezweb/claude-skills/tanstack-router/badges/score.svg" alt="Tanstack Router skill"/></a>

Tanstack Router FAQ

How do I install the Tanstack Router skill?

Run “npx skills add https://github.com/jezweb/claude-skills --skill tanstack-router” in your terminal. The skill is added to your agent's skills directory and picked up automatically on the next run — no restart or extra configuration needed.

What does the Tanstack Router skill do?

| The full SKILL.md on this page shows the exact instructions the skill gives your agent.

Is the Tanstack Router skill free?

Yes. Tanstack Router is a free, open-source skill published from jezweb/claude-skills. As with any third-party skill, review the source repository before installing it into an agent with sensitive access.

Does Tanstack Router work with Claude Code and OpenClaw?

Yes. Skills use the portable SKILL.md format, so Tanstack Router works with Claude Code, OpenClaw, Codex, Hermes, and any other agent that reads SKILL.md skills.

Recommended skills

Browse all →
find-skills logo

find-skills

vercel-labs/skills

2.9M installsInstall
grill-me logo

grill-me

mattpocock/skills

816K installsInstall
frontend-design logo

frontend-design

anthropics/skills

761K installsInstall
grill-with-docs logo

grill-with-docs

mattpocock/skills

695K installsInstall
improve-codebase-architecture logo

improve-codebase-architecture

mattpocock/skills

670K installsInstall
agent-browser logo

agent-browser

vercel-labs/agent-browser

651K installsInstall

Related guides

Hand-picked reading to help you choose, install, and use agent skills.

GuideBest Openclaw Skills 2026GuideHow To Evaluate Openclaw Skill Before InstallingGuideOpenclaw Skills Complete Guide

Skills by category

FrontendBackend & APIsTesting & QASecurityDevOps & CI/CDMCP & ToolingAutomationData & Analysis+27 more

MCP servers by category

MCP & ToolingBackend & APIsData & AnalysisDevOps & CI/CDAutomationSecurityDocsTesting & QA+24 more

Plugins by category

AutomationDevOps & CI/CDData & AnalysisDesign & CreativeSecurityBackend & APIsFrontendTesting & QA+16 more

Marketplaces by category

AutomationData & AnalysisDevOps & CI/CDDesign & CreativeFrontendBackend & APIsTesting & QASecurity+21 more

The Agent Stack

Weekly Claude Code, Agent SDK, and MCP moves worth your time — free.

Claude Market

AI agent skills directory, marketplace, and workflow hub for OpenClaw, Hermes Agent, Claude Code, Codex, and MCP-powered operator stacks.

Independent project, not affiliated with Anthropic.

Resources

  • Browse Skills
  • Browse MCP Servers
  • Browse Plugins
  • Browse Marketplaces
  • Newsletter

More

  • Submit a Tool
  • Create a Skill
  • Advertise
  • Free Tools
  • API
  • Shipping
  • Contact
  • Terms
  • Privacy
© 2026 Claude Market · Not affiliated with Anthropic
Fazier badgeFeatured on Twelve ToolsFeatured on Wired BusinessRemote OpenClaw - Featured on AI Agents DirectoryListed on Turbo0Featured on Uneed