TanStack Router skill for Codex

URLs, search params and loaders that type-check. Codex registers the router for full type inference, models search params as validated, defaulted route state, and keeps loaderDeps limited to the values a loader actually reads. Auth guards live in pathless layouts with beforeLoad, loaders hand caching to TanStack Query through router context, and links use path patterns with params so route renames become type errors.

  • Frontend
  • $tanstack-router
  • 198 lines
  • 2 files
  • ~1,950 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- tanstack-router

Installs to ~/.agents/skills/tanstack-router/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $tanstack-router, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds type-safe React routing with TanStack Router — file-based routes via the Vite plugin and generated routeTree.gen.ts, validated search params with Zod or Standard Schema, loaders with loaderDeps and router context, beforeLoad auth redirects, TanStack Query integration, preloading, code splitting, and pending, error and not-found components. Use when adding routes or search params to a TanStack Router app, when links or params lose type safety, when loaders refetch unexpectedly, or when migrating a React Router SPA to TanStack Router.

Use it when

  • Adding routes or search params to a TanStack Router app
  • Links or params lost type safety
  • Loaders refetch more than expected
  • Moving a React Router SPA to TanStack Router

Use something else when

How to check the result

tsc fails when a link path is broken on purpose, invalid search params fall back to defaults, and changing a non-loader param causes no loader request.

The mistake it prevents

Returning the whole search object from loaderDeps, so any search change, even an unrelated one, re-runs the loader.

~/.agents/skills/tanstack-router/SKILL.md

--- name: tanstack-router description: Builds type-safe React routing with TanStack Router — file-based routes via the Vite plugin and generated routeTree.gen.ts, validated search params… license: MIT ---

Done well means: every <Link>, navigate() and param read type-checks against the generated route tree, search params are validated and defaulted at the route instead of parsed in components, loaders declare exactly which search values they depend on, and auth is enforced in beforeLoad before any child loader runs.

Before you start

  1. Read AGENTS.md and package.json (@tanstack/react-router, @tanstack/router-plugin, optional @tanstack/react-query, @tanstack/react-start). If Start is installed, also use the tanstack-start skill for server code.
  2. Check vite.config.ts for the router plugin and its options (routesDirectory, autoCodeSplitting).
  3. Confirm the router is registered for types (see below). Without registration, links silently accept any string.
  4. Look at src/routes/__root.tsx for router context (query client, auth).

Setup checklist

// 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()], // router plugin before react
});
// src/router.tsx
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";

export const router = createRouter({
  routeTree,
  context: { queryClient, auth: undefined! }, // filled in by <RouterProvider context>
  defaultPreload: "intent",
  defaultPreloadStaleTime: 0, // let TanStack Query own freshness when it is used
  scrollRestoration: true,
});

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

routeTree.gen.ts is generated by the plugin on dev and build. Commit it or not according to the repo convention, but never edit it by hand. Older projects import the plugin as TanStackRouterVite; both names point to the same plugin.

File routes

FileURLNotes
routes/__root.tsxrootcreateRootRouteWithContext<Ctx>()
routes/index.tsx/
routes/projects.tsxlayout for /projects/*renders <Outlet />
routes/projects/index.tsx/projects
routes/projects/$projectId.tsx/projects/:projectId$ marks a param
routes/_auth.tsxpathless layout_ prefix: no URL segment, wraps children
routes/_auth/settings.tsx/settingsprotected by _auth beforeLoad
routes/files/$.tsxsplatvalue in params._splat
routes/-components/ignored- prefix keeps helpers next to routes

Search params as validated state

// src/routes/projects/index.tsx
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";

const searchSchema = z.object({
  page: z.number().int().min(1).catch(1),
  status: z.enum(["active", "archived"]).catch("active"),
  q: z.string().optional(),
});

export const Route = createFileRoute("/projects/")({
  validateSearch: searchSchema, // Standard Schema validators (Zod 3.24+, Valibot, ArkType) work directly
  loaderDeps: ({ search }) => ({ page: search.page, status: search.status, q: search.q }),
  loader: ({ context, deps }) => context.queryClient.ensureQueryData(projectQueries.list(deps)),
  component: ProjectsPage,
});

function ProjectsPage() {
  const search = Route.useSearch();
  const navigate = Route.useNavigate();
  const data = Route.useLoaderData();
  return (
    <>
      <input
        defaultValue={search.q}
        onChange={(e) => navigate({ search: (prev) => ({ ...prev, q: e.target.value || undefined, page: 1 }) })}
      />
      {/* ... */}
    </>
  );
}
  • Use .catch(default) (or fallback() from @tanstack/zod-adapter) so a malformed URL degrades to defaults instead of throwing the route into its error component.
  • Update search with the functional form search: (prev) => ({ ...prev, page: 2 }) so unrelated params survive.
  • loaderDeps must list only the search values the loader uses. Returning the whole search object makes every keystroke in an unrelated param re-run the loader.
  • For params that should persist across links (theme, filters), use search middlewares such as retainSearchParams instead of threading them through every <Link>.

Loaders and context

  • Route context flows from the root: define it with createRootRouteWithContext<{ queryClient: QueryClient; auth: AuthState }>() and pass live values with <RouterProvider router={router} context={{ auth }} />.
  • With TanStack Query, loaders call ensureQueryData (or prefetchQuery for non-critical data) and components read with useSuspenseQuery using the same queryOptions. Query owns caching; the router only triggers loading.
  • Without Query, the router’s built-in cache uses staleTime (default 0) and gcTime; set staleTime on routes whose data is safe to reuse between navigations.
  • Loaders run in parallel for matched routes; beforeLoad runs serially from parent to child, which is why auth belongs there.
// src/routes/_auth.tsx
export const Route = createFileRoute("/_auth")({
  beforeLoad: ({ context, location }) => {
    if (!context.auth.user) {
      throw redirect({ to: "/login", search: { redirect: location.href } });
    }
  },
});
<Link to="/projects/$projectId" params={{ projectId: p.id }} search={{ tab: "settings" }}>
  {p.name}
</Link>
  • Always use to with the route path pattern plus params, never string-built URLs; that is what makes renames a type error instead of a 404.
  • useNavigate({ from: Route.fullPath }) or Route.useNavigate() gives relative navigation with typed search.
  • activeProps / activeOptions={{ exact: true }} handle active styling.

Pending, error and not found

  • pendingComponent with pendingMs (default 1000 ms) and pendingMinMs avoids flashing spinners on fast loads.
  • errorComponent receives error and reset; call router.invalidate() to retry loaders.
  • throw notFound() in a loader renders the nearest notFoundComponent; set defaultNotFoundComponent on the router for a global fallback.

Migrating from React Router (SPA)

Two routers cannot share one history cleanly, so plan a cutover per app (or per separately mounted sub-app) rather than route by route. Map useParams to Route.useParams(), useSearchParams to validated validateSearch + Route.useSearch(), data loaders to loader, and protected route wrappers to a pathless _auth layout with beforeLoad.

Gotchas

  • Forgetting declare module ... Register removes type safety without any error.
  • Running the plugin after @vitejs/plugin-react can break code splitting. Keep the router plugin first.
  • Numbers and booleans in search params are JSON-serialized by default (?page=2 parses to a number); custom parseSearch/stringifySearch changes that for every route.
  • useSearch({ strict: false }) returns a loose union type; prefer the route-bound hook.
  • Search param defaults written in components (search.page ?? 1) drift between components; put them in the schema.

Verification

  • tsc --noEmit passes; deliberately break a to path to confirm links are typed.
  • Open URLs with invalid search params (?page=abc) and confirm defaults apply.
  • Network tab: typing in a non-loader param does not trigger loader requests; hovering a link preloads its route data.
  • Logged out, a protected URL redirects to /login?redirect=... and returns after login.

Output

For each route: path, search schema fields with defaults, loader deps, context used, and auth rule. Note any router-wide default you changed.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "TanStack Router"
  short_description: "URLs, search params and loaders that type-check"
  default_prompt: "$tanstack-router Add page, status and q search params to /projects with defaults and validation"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem