Home / Cursor rules / React Tanstack Router Query

React Tanstack Router Query

React SPA with TanStack Router v1 + TanStack Query v5 — the definitive pattern for zero-loading-spinner routing, type-safe URLs, and cache-first data

Cursor rules (.mdc)Public domain (CC0)
–unranked

Install

mkdir -p .cursor/rules && curl -sL https://raw.githubusercontent.com/PatrickJS/awesome-cursorrules/b044f956f021b6e8877f16781bcfc466a6a120e9/rules/react-tanstack-router-query.mdc -o .cursor/rules/react-tanstack-router-query.mdc

Downloads the file at commit b044f956f021 into .cursor/rules/.

To remove it later:

rm .cursor/rules/react-tanstack-router-query.mdc

Context cost: ≈912 tokens when used.

Rules file

---
description: "React SPA with TanStack Router v1 + TanStack Query v5 — the definitive pattern for zero-loading-spinner routing, type-safe URLs, and cache-first data"
globs: ["src/routes/**/*", "src/queries/**/*", "src/lib/router.ts", "src/lib/queryClient.ts"]
alwaysApply: false
---
You are an expert in React, TanStack Router v1, TanStack Query v5, TypeScript, and Vite.

## Architecture
- TanStack Router: routing, URL state, navigation
- TanStack Query: server state, caching, mutations
- Loader = bridge: prefetches into Query cache before render → zero loading spinners for route data
- Components are pure UI: read from Query cache, trigger mutations

## Setup
```ts
// src/lib/queryClient.ts
export const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: 60_000 } },
})

// src/lib/router.ts
export const router = createRouter({
  routeTree,
  context: { queryClient },
  defaultPreload: 'intent',
  defaultPreloadStaleTime: 0,
})

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

// src/main.tsx
<QueryClientProvider client={queryClient}>
  <RouterProvider router={router} context={{ queryClient }} />
</QueryClientProvider>
```

## Query Definitions
```ts
// src/queries/posts.ts
export const postKeys = {
  all: ['posts'] as const,
  detail: (id: string) => [...postKeys.all, 'detail', id] as const,
  list: (f?: PostFilters) => [...postKeys.all, 'list', f] as const,
}

export const postQueryOptions = (id: string) =>
  queryOptions({ queryKey: postKeys.detail(id), queryFn: () => fetchPost(id) })

export const postsQueryOptions = (filters?: PostFilters) =>
  queryOptions({ queryKey: postKeys.list(filters), queryFn: () => fetchPosts(filters) })
```

## Loader + Component (zero loading state)
```tsx
export const Route = createFileRoute('/posts/$postId')({
  loader: ({ context: { queryClient }, params }) =>
    queryClient.ensureQueryData(postQueryOptions(params.postId)),
  component: PostDetail,
})

function PostDetail() {
  const { postId } = Route.useParams()
  const { data: post } = useQuery(postQueryOptions(postId))  // always in cache from loader
  return <h1>{post!.title}</h1>
}
```

## Search Params → Query Key
```tsx
const searchSchema = z.object({ page: z.number().default(1), q: z.string().optional() })

export const Route = createFileRoute('/posts/')({
  validateSearch: searchSchema,
  loader: ({ context: { queryClient }, location: { search } }) =>
    queryClient.ensureQueryData(postsQueryOptions(search)),
  component: PostsList,
})

function PostsList() {
  const search = Route.useSearch()
  const { data } = useQuery(postsQueryOptions(search))
  // ...
}
```

## Mutations
```tsx
const mutation = useMutation({
  mutationFn: createPost,
  onSuccess: (newPost) => {
    queryClient.setQueryData(postKeys.detail(newPost.id), newPost)  // warm cache
    queryClient.invalidateQueries({ queryKey: postKeys.list() })
    navigate({ to: '/posts/$postId', params: { postId: newPost.id } })  // instant — no spinner
  },
})
```

## Hover Prefetching
```tsx
<Link
  to="/posts/$postId"
  params={{ postId: post.id }}
  onMouseEnter={() => queryClient.prefetchQuery(postQueryOptions(post.id))}
>
  {post.title}
</Link>
```

## Key Rules
- Always define `queryOptions` outside components — never inline inside `useQuery()`
- Never use `useEffect` for data fetching — use loaders or `useQuery`
- Search params are the single source of truth for filter/pagination state
- After mutations: `setQueryData` + `invalidateQueries` for instant UI feedback
- `declare module '@tanstack/react-router'` router registration is required for full type safety

Security

Scanned Nov 4, 2026 at commit b044f956f021, with Cisco's open-source skill-scanner.

None of the patterns we list turned up: shell commands, network requests, reading environment variables, downloading and running code, or installing packages.

A scan lists patterns in the code. It can't prove an add-on is harmless, and it can miss things, so read what you install. How we scan