react-tanstack-router-query — Cursor rule: what it does and setup
Guides AI assistants on structuring React SPAs with TanStack Router v1 and TanStack Query v5 to fetch data in loaders before rendering.
.cursor/rules/react-tanstack-router-query.mdcWhat react-tanstack-router-query does
The rule guides Cursor to generate React SPA architecture using Vite and TypeScript. Route loaders call queryClient.ensureQueryData to populate server state before rendering, which prevents route data spinners. Routing and URL state remain strictly within TanStack Router, while TanStack Query handles caching and mutations.
Code conventions enforce declaring queryOptions factories outside components and rule out useEffect for data fetching. Search parameters act as the single source of truth for filters and pagination, validated through Zod schemas. Successful mutations update the cache immediately via setQueryData before navigating to the target screen.
The rule activates for src/routes/**/*, src/queries/**/*, src/lib/router.ts, and src/lib/queryClient.ts. Registering the router via declare module '@tanstack/react-router' is mandatory, because TypeScript cannot check route paths and parameters without it.
What it helps with
- Integrate TanStack Router v1 with TanStack Query v5 without route spinners
- Move data fetching logic out of components into route loaders
- Tie URL search parameters directly to Query cache keys
- Warm the query cache on mutation success before triggering navigation
Example prompts for the agent
- Create route
src/routes/posts/$postId.tsxwith a loader usingpostQueryOptionsand render the post. - Implement a paginated posts list using URL search parameters validated with Zod.
- Add a post creation mutation that warms the cache with
setQueryDataand navigates to the new item.
How to install react-tanstack-router-query
Manually: put the files in place
.cursor/rules/react-tanstack-router-query.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/react-tanstack-router-query.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/react-tanstack-router-query.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/react-tanstack-router-query.mdManually: put the files in place
.clinerules/react-tanstack-router-query.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about react-tanstack-router-query
How to install react-tanstack-router-query in Cursor?
Place the rule file at .cursor/rules/react-tanstack-router-query.mdc in your project repository.
What files trigger this rule?
Cursor evaluates it when working in src/routes/**/*, src/queries/**/*, src/lib/router.ts, or src/lib/queryClient.ts.
Why does this pattern avoid useEffect for data fetching?
Route loaders prefetch data with queryClient.ensureQueryData before UI mounts, so components read ready data straight from cache.
Source text
react-tanstack-router-query.mdc · CC0-1.0
---
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
© PatrickJS · CC0-1.0 · PatrickJS/awesome-cursorrules