agentleFS
FirzusFirzus/agent-skills/skills/web/tanstack-router/SKILL.md

tanstack-router

Skill4 starsChanged 20 days agoMITView on GitHub

TanStack Router v1 for React — file-based routing, type-safe navigation, validated search params, loaders and router context. Use when working in a project that uses `@tanstack/react-router` — routeTree.gen.ts, createFileRoute, Link/useNavigate, validateSearch, loaderDeps, route masking, or code splitting.

14 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use accessibility, review and 1 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • ElasticSkill

    accessibility

    Accessibility guidance for Kibana. Use this skill when working with or reviewing EUI components, resolving a11y-related (@elastic/eui) ESLint issues, and ensuring proper use of ARIA attributes, focus management, keyboard interactions, and accessible naming conventions.

    Also in 3 projects that use this

  • SentrySkill

    review

    Fixture review skill.

    Also in 4 projects that use this

  • GoogleSkill

    improve-codebase-architecture

    Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick.

    Also in 3 projects that use this

The file

Show its rules and text

We split this skill into the rules it gives an agent

16 rules. 0 are common, 1 is distinctive, and 15 are very specific to this project.

  • 0

    Common

    Most of the 11 edited versions we read keep these.

  • 1

    Distinctive

    This author's own choices. Worth a look.

  • 15

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Prefer the project's existing route layout, validation library, and data layer.

    line 18
  2. Very specific:

    Check whether routes come from the file-based generator (routeTree.gen.ts plus the bundler plugin) or from code-based createRoute calls.

    line 45
  3. Very specific:

    Check the bundler config for tanstackRouter({ autoCodeSplitting: true }) — it decides whether .lazy.tsx files are needed at all.

    line 48
  4. Very specific:

    If a queryClient sits in the router context, treat the router as a coordinator, not a cache (tanstack-query.md).

    line 50
  5. Very specific:

    Every non-root route is created with createFileRoute, whose single string argument is written and maintained by the generator.

    line 80
  6. Very specific:

    Every navigation is relative and has both an origin (from) and a destination (to).

    line 134
  7. Very specific:

    Never interpolate values into to.

    line 146
  8. Very specific:

    Prefer <Link> for anything clickable — it renders a real <a href>, so cmd/ctrl-click and middle-click work.

    line 152
  9. Very specific:

    Write search params with <Link search={prev => ...}> or navigate({ search }).

    line 207
  10. Very specific:

    Search params are deliberately absent from loader arguments.

    line 226
  11. Very specific:

    After a load failure, prefer router.invalidate() over reset(): it reruns the loader and resets the boundary together.

    line 255
  12. Very specific:

    Only routes that render an <Outlet /> can host a not-found boundary, so give the root route a notFoundComponent or the router a …

    line 261
  13. Very specific:

    Prefer autoCodeSplitting: true in the bundler plugin.

    line 273
  14. Very specific:

    When a component lives outside its route file, reach for getRouteApi('/path') instead of importing the Route object — same typed hooks, no …

    line 278
  15. Very specific:

    No value is interpolated into to.

    line 287
  16. Very specific:

    Every route reading query state defines validateSearch, with fallbacks for malformed input and an adapter where the validation library …

    line 289

“Common” is measured: kept by at least 6 in 10 of the 11 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

TanStack Router

Reference for TanStack Router v1 with React. This skill covers client-side routing only. For the full-stack framework built on top of this router — server routes, server functions, SSR wiring, deployment — use the sibling tanstack-start skill. Everything here still applies inside a Start app: Start uses this router underneath.

Prefer the project's existing route layout, validation library, and data layer; apply these rules on top so route identity, type inference, and cache keys stay predictable.

Branch-specific references, loaded on demand:

  • file-routing.md — naming conventions, the generated route tree, layouts, groups, splat and optional params.
  • search-params.md — validateSearch, schema adapters, search middlewares, reading and writing search state.
  • data-loading.md — loaders, loaderDeps, caching options, router context, pending/error/not-found components, deferred data.
  • navigation.md — Link, useNavigate, relative paths, linkOptions, preloading, route masking.
  • tanstack-query.md — router as coordinator, the SSR query integration, and the split of responsibilities with TanStack Query.

First Checks

  1. Confirm @tanstack/react-router is installed and read the lockfile version. This skill targets v1.
  2. Find the router instance (usually src/router.tsx). Read createRouter for defaultPreload, defaultPreloadStaleTime, defaultStaleTime, notFoundMode, and routeMasks — these change the correct advice for loaders and links.
  3. Confirm the Register declaration merging block exists. Without it, top-level Link, useNavigate, and useSearch lose all route types.
  4. Check whether routes come from the file-based generator (routeTree.gen.ts plus the bundler plugin) or from code-based createRoute calls. Never edit routeTree.gen.ts by hand.
  5. Check the bundler config for tanstackRouter({ autoCodeSplitting: true }) — it decides whether .lazy.tsx files are needed at all.
  6. If a queryClient sits in the router context, treat the router as a coordinator, not a cache (tanstack-query.md).

Registering The Router

Type safety across the whole project depends on two things: the generated route tree, and declaration merging.

// src/router.tsx
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export const router = createRouter({
  routeTree,
  defaultPreload: 'intent',
})

declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

Without the Register block, every bare import from the library falls back to loose types and autocomplete for to, params, and search disappears.

Anatomy Of A Route

Every non-root route is created with createFileRoute, whose single string argument is written and maintained by the generator. Do not hand-edit it; move or rename the file and let the plugin update it.

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

export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params }) => fetchPost(params.postId),
  component: PostComponent,
})

function PostComponent() {
  const { postId } = Route.useParams()
  const post = Route.useLoaderData()

  return <h1>{post.title}</h1>
}

The root route is different: createRootRoute(), or createRootRouteWithContext<T>()() when the router carries context. It has no path, is always matched, and its component always renders. It does not support code splitting.

The Route object exposes typed hooks — useParams, useSearch, useLoaderData, useLoaderDeps, useMatch, useRouteContext. Use them instead of the bare exports whenever you are inside the route's own component.

File Naming, In One Table

Full detail in file-routing.md; this is the working subset.

TokenMeaning
__root.tsxRoot route file, at the top of routesDirectory.
.Nesting separator — posts.$postId.tsx nests under posts.tsx.
$labelDynamic segment, captured into params.label.
$ aloneSplat route; the rest of the pathname lands in params._splat.
{-$label}Optional segment — matches with or without it.
_ prefixPathless layout route: wraps children without adding a path.
_ suffixNon-nested route: escapes its parent's component tree.
- prefixExcluded from the route tree; use it to colocate components.
(folder)Route group — organizational only, contributes no path segment.
indexMatches the parent path exactly.
route.tsxThe route file for its containing directory.
[x]Escapes a special character — script[.]js.tsx becomes /script.js.

Directory nesting and . nesting are interchangeable and mixable. Choose per subtree: directories for wide hierarchies, dots for a few deep ones.

Type-Safe Navigation

Every navigation is relative and has both an origin (from) and a destination (to). Without from, the router assumes the root and only autocompletes absolute paths.

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

Rules that prevent most navigation bugs:

  • Never interpolate values into to. Path params go in params, query state in search, the fragment in hash. to stays a literal route pattern.
  • params and search accept a function of the previous value — use it to preserve existing state instead of rebuilding it.
  • to="." reloads the current (or from) location; to=".." goes to the parent. Both are relative to from when supplied.
  • Prefer <Link> for anything clickable — it renders a real <a href>, so cmd/ctrl-click and middle-click work. Reach for useNavigate only for side-effect navigation, such as after a successful submit.
  • redirect() thrown from beforeLoad is the right tool for guards; a client-side navigation is not a substitute for a server redirect.
  • Pass from={Route.fullPath} rather than a hand-written string, so refactors follow.

Active state is available three ways: activeProps/inactiveProps, the data-status="active" attribute, and a function child receiving { isActive }. By default a link is active on pathname prefix match with search params compared inclusively; pass activeOptions={{ exact: true }} for a home link.

See navigation.md for linkOptions, preloading strategies, and route masking.

Search Params Are Typed State

TanStack Router parses the query string into JSON, not flat strings, and validateSearch is the boundary that turns it into a type your app can trust. The resulting type flows into the route's own options and every child route.

import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'

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

export const Route = createFileRoute('/shop/products')({
  validateSearch: productSearchSchema,
  component: ProductList,
})

function ProductList() {
  const { page, filter, sort } = Route.useSearch()
  return <div>...</div>
}

Key points:

  • validateSearch accepts a function or any object with a parse method, so a schema can be passed directly.
  • Malformed params come from user-editable text. Prefer a fallback (.catch()) over a hard failure unless an error screen is genuinely the right UX — a throw triggers onError with error.routerCode set to VALIDATE_SEARCH and renders the errorComponent.
  • With Zod v3 and .default(), search becomes required on every Link to that route. Use @tanstack/zod-adapter's zodValidator (plus its fallback helper) to keep input and output types distinct. Zod v4, Valibot, ArkType, and Effect/Schema implement Standard Schema and need no adapter.
  • Write search params with <Link search={prev => ...}> or navigate({ search }). From a component shared across routes, use to="." or strict: false to opt into looser types deliberately.

Loaders, Context, Dependencies

A loader runs before the route renders, in parallel with sibling loaders, and its result is cached under the route's parsed pathname plus whatever loaderDeps returns.

export const Route = createFileRoute('/posts')({
  validateSearch: z.object({ offset: z.number().catch(0) }),
  loaderDeps: ({ search: { offset } }) => ({ offset }),
  loader: ({ deps: { offset }, context, abortController }) =>
    context.fetchPosts({ offset, signal: abortController.signal }),
})
  • Search params are deliberately absent from loader arguments. Anything the loader reads from search must pass through loaderDeps, otherwise the cache and preloading key on the wrong identity.
  • Return only the deps you actually use. loaderDeps: ({ search }) => search reloads the route when any unrelated param changes.
  • loaderDeps and params.parse run during route planning, possibly more than once. Keep them deterministic and side-effect-free.
  • beforeLoad runs before the loader and before all child beforeLoads. Its return value merges into the context for the route and its descendants. Throw redirect() here for guards, and re-throw redirects caught in a try/catch — isRedirect(error) tells them apart from real failures.
  • Context is the dependency-injection channel: type it with createRootRouteWithContext<T>(), seed it in createRouter({ context }), and extend it per route via beforeLoad.

Defaults worth knowing: staleTime is 0 (stale immediately, revalidated in the background), preloaded data is fresh for 30 s, garbage collection windows are 5 min, the pending component appears after 1 s and then stays for at least 500 ms. Full options in data-loading.md.

Errors, Pending, Not Found

SituationRoute optionNotes
Loader threwerrorComponentReceives error and reset.
Loader is slowpendingComponentGated by pendingMs / pendingMinMs.
Resource missingnotFoundComponentThrow notFound() from the loader.
Side-effect on failureonError / onCatchLogging, reporting.
  • After a load failure, prefer router.invalidate() over reset(): it reruns the loader and resets the boundary together.
  • Throw notFound() in the loader, not in the component — that keeps loader data correctly typed and avoids a flicker.
  • Inside notFoundComponent, useLoaderData may be undefined; useParams, useSearch, and useRouteContext remain safe.
  • Only routes that render an <Outlet /> can host a not-found boundary, so give the root route a notFoundComponent or the router a defaultNotFoundComponent.

Code Splitting

The router splits critical config (path parsing, validateSearch, beforeLoad, loader, context) from non-critical config (component, errorComponent, pendingComponent, notFoundComponent). The loader is intentionally not split: it is already an async boundary and it is the most valuable thing to preload.

Prefer autoCodeSplitting: true in the bundler plugin. It requires file-based routing with a supported bundler and does not work with the CLI alone. Otherwise move the non-critical exports into a <route>.lazy.tsx file using createLazyFileRoute, which accepts only those four component options.

When a component lives outside its route file, reach for getRouteApi('/path') instead of importing the Route object — same typed hooks, no circular import.

Review Checklist

  • The router is registered via declare module declaration merging, and routeTree.gen.ts is generated, not hand-edited.
  • Route file names express the intended tree: _ for pathless layouts, (...) for grouping, - for colocated non-route files, index for exact matches.
  • No value is interpolated into to; params, search, and hash use their own props, and from={Route.fullPath} is used for relative navigation.
  • Every route reading query state defines validateSearch, with fallbacks for malformed input and an adapter where the validation library needs one.
  • loaderDeps returns exactly the search values the loader consumes — no more.
  • beforeLoad guards throw redirect() and re-throw caught redirects via isRedirect; the guard is not treated as an authorization boundary for data.
  • staleTime, preloadStaleTime, and gcTime are set deliberately per route where the defaults do not fit; defaultPreloadStaleTime: 0 when an external cache owns freshness.
  • Error, pending, and not-found components exist at the levels that render an <Outlet />; loader-failure retries call router.invalidate().
  • Components outside their route file use getRouteApi, not a Route import.
  • Shared components either narrow with from/to or opt into strict: false explicitly — never rely on the accidental union.
  • Full-stack concerns (server routes, server functions, SSR setup) live in the tanstack-start skill, not here.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.