react-tanstack-router-query-cursorrules-prompt-file — Cursor rule
This rule describes a React SPA setup where TanStack Router handles navigation and TanStack Query stores server data. Use it to fetch route data before rendering a page.
.cursor/rules/react-tanstack-router-query-cursorrules-prompt-file.mdcWhat react-tanstack-router-query-cursorrules-prompt-file does
The rule separates routing from server state: TanStack Router manages navigation and URL parameters, while TanStack Query handles asynchronous requests and caching. Route loaders call queryClient.ensureQueryData before a page renders; components then read the cached result with useQuery. This avoids a route-level loading spinner, while Suspense covers loading inside components.
It includes a suggested routes, queries, api, and lib layout, setup for the router and QueryClient, and query option factories shared by loaders and components. Other examples cover Zod validation for search parameters, cache updates after mutations, and access checks with beforeLoad. The rule has alwaysApply: false, so Cursor does not apply it unconditionally.
What it helps with
- fetch route data before rendering a page
- keep query keys and fetch functions together
- use URL parameters to drive filters and pagination
- update cached data after creating a post
- guard routes against unauthenticated access
Example prompts for the agent
- Set up a React SPA that fetches route data through TanStack Query before rendering.
- Connect the post list's URL search parameters to its query key and fetch function.
- Add an authentication check before protected routes load.
How to install react-tanstack-router-query-cursorrules-prompt-file
Manually: put the files in place
.cursor/rules/react-tanstack-router-query-cursorrules-prompt-file.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/react-tanstack-router-query-cursorrules-prompt-file.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-cursorrules-prompt-file.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-cursorrules-prompt-file.mdManually: put the files in place
.clinerules/react-tanstack-router-query-cursorrules-prompt-file.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about react-tanstack-router-query-cursorrules-prompt-file
How do I install react-tanstack-router-query-cursorrules-prompt-file in Cursor?
Add the rule file to your project at .cursor/rules/react-tanstack-router-query-cursorrules-prompt-file.mdc. Cursor can then use its instructions.
What does react-tanstack-router-query-cursorrules-prompt-file do?
It describes how a React SPA can combine TanStack Router for navigation with TanStack Query for server data, caching, and route loaders.
How does the rule load route data before rendering?
A route loader calls queryClient.ensureQueryData to populate the cache. The component reads that result with useQuery using the same query options.
Source text
react-tanstack-router-query-cursorrules-prompt-file.mdc · CC0-1.0
---
description: "Cursor rules for React SPAs combining TanStack Router v1 and TanStack Query v5 for zero-loading-spinner routing and type-safe server state."
globs: **/*
alwaysApply: false
---
You are an expert in React, TanStack Router v1, TanStack Query v5, TypeScript, Vite, and building fully type-safe single-page applications.
# React + TanStack Router + TanStack Query Guidelines
## Architecture Overview
- TanStack Router handles all routing, URL state, and navigation
- TanStack Query manages all server state, caching, and async data
- React components are pure UI — they read from Query cache and trigger mutations
- Loaders bridge Router and Query: they prefetch into the Query cache before render
- This eliminates loading spinners for route-level data; Suspense handles component-level loading
## Project Setup
```
src/
routes/
__root.tsx
index.tsx
posts/
index.tsx
$postId.tsx
queries/ ← Query definitions (queryOptions factories)
posts.ts
users.ts
api/ ← API client functions (fetchers)
posts.ts
users.ts
lib/
queryClient.ts
router.ts
main.tsx
```
## QueryClient + Router Setup
```ts
// src/lib/queryClient.ts
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60,
retry: (count, error: any) => error?.status !== 404 && count < 2,
},
},
})
```
```tsx
// src/lib/router.ts
import { createRouter } from '@tanstack/react-router'
import { routeTree } from '../routeTree.gen'
import { queryClient } from './queryClient'
export const router = createRouter({
routeTree,
context: { queryClient },
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
```
```tsx
// src/main.tsx
import { RouterProvider } from '@tanstack/react-router'
import { QueryClientProvider } from '@tanstack/react-query'
import { router } from './lib/router'
import { queryClient } from './lib/queryClient'
ReactDOM.createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} context={{ queryClient }} />
</QueryClientProvider>
)
```
## Query Definitions (queryOptions factories)
- Co-locate query key, fetcher, and staleTime in one place
- Share between Router loaders and component hooks
```ts
// src/queries/posts.ts
import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query'
import { fetchPost, fetchPosts } from '../api/posts'
export const postKeys = {
all: ['posts'] as const,
lists: () => [...postKeys.all, 'list'] as const,
list: (filters?: PostFilters) => [...postKeys.lists(), filters] as const,
details: () => [...postKeys.all, 'detail'] as const,
detail: (id: string) => [...postKeys.details(), id] as const,
}
export const postDetailQueryOptions = (id: string) =>
queryOptions({
queryKey: postKeys.detail(id),
queryFn: () => fetchPost(id),
staleTime: 1000 * 60 * 5,
})
export const postsListQueryOptions = (filters?: PostFilters) =>
queryOptions({
queryKey: postKeys.list(filters),
queryFn: () => fetchPosts(filters),
staleTime: 1000 * 60,
})
```
## Router Loader + Query Integration
- Loaders call `queryClient.ensureQueryData` — populates cache, renders immediately without spinner
- Components then call `useQuery` with the same options — reads from cache synchronously
```tsx
// src/routes/posts/$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { postDetailQueryOptions } from '../../queries/posts'
export const Route = createFileRoute('/posts/$postId')({
loader: ({ context: { queryClient }, params }) =>
queryClient.ensureQueryData(postDetailQueryOptions(params.postId)),
errorComponent: ({ error }) => <ErrorMessage error={error} />,
pendingComponent: PostSkeleton,
component: PostDetail,
})
function PostDetail() {
const { postId } = Route.useParams()
// data is already in cache from loader — no loading state
const { data: post } = useQuery(postDetailQueryOptions(postId))
return <article><h1>{post!.title}</h1></article>
}
```
## Search Params + Query Integration
- Use TanStack Router search params as the source of truth for filter/pagination state
- Pass search params into queryOptions to drive query key and fetcher
```tsx
// src/routes/posts/index.tsx
import { createFileRoute, Link } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { z } from 'zod'
import { postsListQueryOptions } from '../../queries/posts'
const searchSchema = z.object({
page: z.number().int().min(1).default(1),
category: z.string().optional(),
})
export const Route = createFileRoute('/posts/')({
validateSearch: searchSchema,
loader: ({ context: { queryClient }, location: { search } }) =>
queryClient.ensureQueryData(postsListQueryOptions(search)),
component: PostsList,
})
function PostsList() {
const search = Route.useSearch()
const navigate = Route.useNavigate()
const { data: posts } = useQuery(postsListQueryOptions(search))
return (
<div>
{posts?.map(post => (
<Link key={post.id} to="/posts/$postId" params={{ postId: post.id }}>
{post.title}
</Link>
))}
<button onClick={() => navigate({ search: { ...search, page: search.page + 1 } })}>
Next Page
</button>
</div>
)
}
```
## Mutations
```tsx
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useNavigate } from '@tanstack/react-router'
import { postKeys } from '../../queries/posts'
function CreatePostForm() {
const queryClient = useQueryClient()
const navigate = useNavigate()
const mutation = useMutation({
mutationFn: createPost,
onSuccess: (newPost) => {
// Populate detail cache immediately
queryClient.setQueryData(postKeys.detail(newPost.id), newPost)
// Invalidate list queries
queryClient.invalidateQueries({ queryKey: postKeys.lists() })
// Navigate to new post (no loading — cache is warm)
navigate({ to: '/posts/$postId', params: { postId: newPost.id } })
},
})
return (/* form JSX */)
}
```
## Authentication Pattern
```tsx
// src/routes/__root.tsx
import { createRootRouteWithContext } from '@tanstack/react-router'
export interface RouterContext {
queryClient: QueryClient
auth: { isAuthenticated: boolean; user: User | null }
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootLayout,
})
// src/routes/_auth.tsx (pathless layout for protected routes)
export const Route = createFileRoute('/_auth')({
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: '/login', search: { redirect: location.pathname } })
}
},
})
```
## Prefetching on Hover
```tsx
function PostCard({ post }: { post: Post }) {
const queryClient = useQueryClient()
return (
<Link
to="/posts/$postId"
params={{ postId: post.id }}
onMouseEnter={() => queryClient.prefetchQuery(postDetailQueryOptions(post.id))}
>
{post.title}
</Link>
)
}
```
## DevTools (Development Only)
```tsx
// In __root.tsx
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
// Inside component
{import.meta.env.DEV && (
<>
<TanStackRouterDevtools position="bottom-left" />
<ReactQueryDevtools buttonPosition="bottom-right" />
</>
)}
```
## Key Rules
- Always define `queryOptions` outside of components — not inline in `useQuery()`
- Never use `useEffect` to fetch data — use loaders or `useQuery`
- Always type router context — `declare module '@tanstack/react-router'` registration is required
- Search params are the only source of truth for URL-driven filter state
- Mutations should `setQueryData` + `invalidateQueries`, not just invalidate, for instant UI feedback
© PatrickJS · CC0-1.0 · PatrickJS/awesome-cursorrules