# Setup Tanstack Router

> Auto-generate TanStack Router route tree and enforce router patterns via PostToolUse hooks. Bans react-router-dom, window.location, untyped hooks. Use when setting up TanStack Router or file-based routing.

- Skill: `redpanda-data/setup-tanstack-router` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add redpanda-data/setup-tanstack-router`
- Raw SKILL.md: https://api.skillmd.com/api/skills/redpanda-data/setup-tanstack-router/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: redpanda-data (https://skillmd.com/u/redpanda-data)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/redpanda-data/setup-tanstack-router

---


# TanStack Router Enforcement

## What This Catches

- Ban `react-router-dom` imports
- Ban `window.location` navigation (block) + reads (warn)
- Warn `window.location.reload()` -- suggest `router.invalidate()`
- Ban `strict: false` in router hooks
- Ban untyped `useParams()`/`useSearch()`/`useLoaderData()`/`useRouteContext()` missing `{ from }`
- Ban `URLSearchParams` -- suggest nuqs
- Warn exported components from route files (break code splitting)
- Require `validateSearch` when `useSearch` in route files
- Warn when Query-primed loaders are consumed via `useLoaderData` instead of `useQuery`/`useSuspenseQuery`
- Warn when router uses `queryClient` context without `defaultPreloadStaleTime: 0`
- Warn when router uses `queryClient` context without `createRootRouteWithContext`

Auto-regen route tree on route file change.

## TanStack Router + Query

When a route needs server data, prefer this ownership split:

- **Router loader**: start fetching early after navigation intent.
- **TanStack Query**: own cache, refetch, invalidation, and garbage collection.
- **Component**: read via `useQuery()` or `useSuspenseQuery()` so Query has an active observer.

Do **not** enforce suspense globally. Choose per field/page:

- `useSuspenseQuery()` for blocking, page-critical data that should use route pending/error boundaries.
- `useQuery()` for deferred or secondary data with inline loading/empty/error states.

```tsx
export const Route = createFileRoute('/dashboards/$dashboardId')({
  loader: ({ context, params }) => {
    context.queryClient.prefetchQuery(dashboardQueryOptions(params.dashboardId))
  },
  component: Dashboard,
})

function Dashboard() {
  const params = Route.useParams()
  const dashboard = useSuspenseQuery(dashboardQueryOptions(params.dashboardId))
  const widgetCount = useQuery(widgetCountQueryOptions(params.dashboardId))

  return <DashboardView dashboard={dashboard.data} widgetCount={widgetCount.data} />
}
```

Router setup when Query owns cache:

```tsx
const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>()({
  component: RootLayout,
})

const router = createRouter({
  routeTree,
  context: { queryClient },
  defaultPreloadStaleTime: 0,
  defaultPendingComponent: DefaultLoader,
  defaultErrorComponent: DefaultError,
})
```

Avoid `Route.useLoaderData()` for Query-loaded data. It bypasses Query observers, so focus refetch, invalidation refetch, and cache retention can behave surprisingly.

## Customization

Routes dir pattern default `/routes/`. Update grep pattern in hook scripts if project use different convention:

```bash
if ! echo "$file_path" | grep -qE '/routes/'; then    # default
if ! echo "$file_path" | grep -qE '/pages/'; then     # pages-based
if ! echo "$file_path" | grep -qE '/app/routes/'; then # nested
```

## Type-Safe Search Params with nuqs

```tsx
import { useQueryState, parseAsInteger, parseAsString } from 'nuqs'

function UsersPage() {
  const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1))
  const [filter, setFilter] = useQueryState('filter', parseAsString)
}
```

Setup (install, config, verify): see [SETUP.md](SETUP.md).
