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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- tanstack-router Installs to ~/.agents/skills/tanstack-router/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install tanstack-router The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project tanstack-router Installs to .agents/skills/tanstack-router/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/tanstack-router.zipunzip -o tanstack-router.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) tanstack-router PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- You need server functions, SSR or full-stack setup: use tanstack-start
- The project uses React Router framework mode: use react-router-v7
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.
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
- Read
AGENTS.mdandpackage.json(@tanstack/react-router,@tanstack/router-plugin, optional@tanstack/react-query,@tanstack/react-start). If Start is installed, also use thetanstack-startskill for server code. - Check
vite.config.tsfor the router plugin and its options (routesDirectory,autoCodeSplitting). - Confirm the router is registered for types (see below). Without registration, links silently accept any string.
- Look at
src/routes/__root.tsxfor 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
| File | URL | Notes |
|---|---|---|
routes/__root.tsx | root | createRootRouteWithContext<Ctx>() |
routes/index.tsx | / | |
routes/projects.tsx | layout for /projects/* | renders <Outlet /> |
routes/projects/index.tsx | /projects | |
routes/projects/$projectId.tsx | /projects/:projectId | $ marks a param |
routes/_auth.tsx | pathless layout | _ prefix: no URL segment, wraps children |
routes/_auth/settings.tsx | /settings | protected by _auth beforeLoad |
routes/files/$.tsx | splat | value 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)(orfallback()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. loaderDepsmust list only the search values the loader uses. Returning the wholesearchobject 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
retainSearchParamsinstead 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(orprefetchQueryfor non-critical data) and components read withuseSuspenseQueryusing the samequeryOptions. Query owns caching; the router only triggers loading. - Without Query, the router’s built-in cache uses
staleTime(default 0) andgcTime; setstaleTimeon routes whose data is safe to reuse between navigations. - Loaders run in parallel for matched routes;
beforeLoadruns 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 } });
}
},
});
Navigation and links
<Link to="/projects/$projectId" params={{ projectId: p.id }} search={{ tab: "settings" }}>
{p.name}
</Link>
- Always use
towith the route path pattern plusparams, never string-built URLs; that is what makes renames a type error instead of a 404. useNavigate({ from: Route.fullPath })orRoute.useNavigate()gives relative navigation with typed search.activeProps/activeOptions={{ exact: true }}handle active styling.
Pending, error and not found
pendingComponentwithpendingMs(default 1000 ms) andpendingMinMsavoids flashing spinners on fast loads.errorComponentreceiveserrorandreset; callrouter.invalidate()to retry loaders.throw notFound()in a loader renders the nearestnotFoundComponent; setdefaultNotFoundComponenton 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 ... Registerremoves type safety without any error. - Running the plugin after
@vitejs/plugin-reactcan break code splitting. Keep the router plugin first. - Numbers and booleans in search params are JSON-serialized by default (
?page=2parses to a number); customparseSearch/stringifySearchchanges 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 --noEmitpasses; deliberately break atopath 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.yaml
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.