Skip to content

Commit 4bfb2ac

Browse files
usm4nhafeezPatrickJS
authored andcommitted
feat: add React TanStack Router and Query cursorrules
1 parent d83652c commit 4bfb2ac

5 files changed

Lines changed: 521 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ By creating a `.cursorrules` file in your project's root directory, you can leve
183183
- [React (Redux, TypeScript)](./rules/react-redux-typescript-cursorrules-prompt-file/.cursorrules) - Cursor rules for React development with Redux and TypeScript integration.
184184
- [React (MobX)](./rules/react-mobx-cursorrules-prompt-file/.cursorrules) - Cursor rules for React development with MobX integration.
185185
- [React (React Query)](./rules/react-query-cursorrules-prompt-file/.cursorrules) - Cursor rules for React development with React Query integration.
186+
- [React (TanStack Router + Query)](./rules/react-tanstack-router-query-cursorrules-prompt-file/.cursorrules) - Cursor rules for React SPAs combining TanStack Router v1 and TanStack Query v5 for zero-loading-spinner routing and type-safe server state.
186187

187188
### Database and API
188189

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
---
2+
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
3+
globs: ["src/routes/**/*", "src/queries/**/*", "src/lib/router.ts", "src/lib/queryClient.ts"]
4+
alwaysApply: false
5+
---
6+
7+
You are an expert in React, TanStack Router v1, TanStack Query v5, TypeScript, and Vite.
8+
9+
## Architecture
10+
- TanStack Router: routing, URL state, navigation
11+
- TanStack Query: server state, caching, mutations
12+
- Loader = bridge: prefetches into Query cache before render → zero loading spinners for route data
13+
- Components are pure UI: read from Query cache, trigger mutations
14+
15+
## Setup
16+
```ts
17+
// src/lib/queryClient.ts
18+
export const queryClient = new QueryClient({
19+
defaultOptions: { queries: { staleTime: 60_000 } },
20+
})
21+
22+
// src/lib/router.ts
23+
export const router = createRouter({
24+
routeTree,
25+
context: { queryClient },
26+
defaultPreload: 'intent',
27+
defaultPreloadStaleTime: 0,
28+
})
29+
30+
declare module '@tanstack/react-router' {
31+
interface Register { router: typeof router }
32+
}
33+
34+
// src/main.tsx
35+
<QueryClientProvider client={queryClient}>
36+
<RouterProvider router={router} context={{ queryClient }} />
37+
</QueryClientProvider>
38+
```
39+
40+
## Query Definitions
41+
```ts
42+
// src/queries/posts.ts
43+
export const postKeys = {
44+
all: ['posts'] as const,
45+
detail: (id: string) => [...postKeys.all, 'detail', id] as const,
46+
list: (f?: PostFilters) => [...postKeys.all, 'list', f] as const,
47+
}
48+
49+
export const postQueryOptions = (id: string) =>
50+
queryOptions({ queryKey: postKeys.detail(id), queryFn: () => fetchPost(id) })
51+
52+
export const postsQueryOptions = (filters?: PostFilters) =>
53+
queryOptions({ queryKey: postKeys.list(filters), queryFn: () => fetchPosts(filters) })
54+
```
55+
56+
## Loader + Component (zero loading state)
57+
```tsx
58+
export const Route = createFileRoute('/posts/$postId')({
59+
loader: ({ context: { queryClient }, params }) =>
60+
queryClient.ensureQueryData(postQueryOptions(params.postId)),
61+
component: PostDetail,
62+
})
63+
64+
function PostDetail() {
65+
const { postId } = Route.useParams()
66+
const { data: post } = useQuery(postQueryOptions(postId)) // always in cache from loader
67+
return <h1>{post!.title}</h1>
68+
}
69+
```
70+
71+
## Search Params → Query Key
72+
```tsx
73+
const searchSchema = z.object({ page: z.number().default(1), q: z.string().optional() })
74+
75+
export const Route = createFileRoute('/posts/')({
76+
validateSearch: searchSchema,
77+
loader: ({ context: { queryClient }, location: { search } }) =>
78+
queryClient.ensureQueryData(postsQueryOptions(search)),
79+
component: PostsList,
80+
})
81+
82+
function PostsList() {
83+
const search = Route.useSearch()
84+
const { data } = useQuery(postsQueryOptions(search))
85+
// ...
86+
}
87+
```
88+
89+
## Mutations
90+
```tsx
91+
const mutation = useMutation({
92+
mutationFn: createPost,
93+
onSuccess: (newPost) => {
94+
queryClient.setQueryData(postKeys.detail(newPost.id), newPost) // warm cache
95+
queryClient.invalidateQueries({ queryKey: postKeys.list() })
96+
navigate({ to: '/posts/$postId', params: { postId: newPost.id } }) // instant — no spinner
97+
},
98+
})
99+
```
100+
101+
## Hover Prefetching
102+
```tsx
103+
<Link
104+
to="/posts/$postId"
105+
params={{ postId: post.id }}
106+
onMouseEnter={() => queryClient.prefetchQuery(postQueryOptions(post.id))}
107+
>
108+
{post.title}
109+
</Link>
110+
```
111+
112+
## Key Rules
113+
- Always define `queryOptions` outside components — never inline inside `useQuery()`
114+
- Never use `useEffect` for data fetching — use loaders or `useQuery`
115+
- Search params are the single source of truth for filter/pagination state
116+
- After mutations: `setQueryData` + `invalidateQueries` for instant UI feedback
117+
- `declare module '@tanstack/react-router'` router registration is required for full type safety
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
You are an expert in React, TanStack Router v1, TanStack Query v5, TypeScript, Vite, and building fully type-safe single-page applications.
2+
3+
# React + TanStack Router + TanStack Query Guidelines
4+
5+
## Architecture Overview
6+
- TanStack Router handles all routing, URL state, and navigation
7+
- TanStack Query manages all server state, caching, and async data
8+
- React components are pure UI — they read from Query cache and trigger mutations
9+
- Loaders bridge Router and Query: they prefetch into the Query cache before render
10+
- This eliminates loading spinners for route-level data; Suspense handles component-level loading
11+
12+
## Project Setup
13+
```
14+
src/
15+
routes/
16+
__root.tsx
17+
index.tsx
18+
posts/
19+
index.tsx
20+
$postId.tsx
21+
queries/ ← Query definitions (queryOptions factories)
22+
posts.ts
23+
users.ts
24+
api/ ← API client functions (fetchers)
25+
posts.ts
26+
users.ts
27+
lib/
28+
queryClient.ts
29+
router.ts
30+
main.tsx
31+
```
32+
33+
## QueryClient + Router Setup
34+
```ts
35+
// src/lib/queryClient.ts
36+
import { QueryClient } from '@tanstack/react-query'
37+
38+
export const queryClient = new QueryClient({
39+
defaultOptions: {
40+
queries: {
41+
staleTime: 1000 * 60,
42+
retry: (count, error: any) => error?.status !== 404 && count < 2,
43+
},
44+
},
45+
})
46+
```
47+
48+
```tsx
49+
// src/lib/router.ts
50+
import { createRouter } from '@tanstack/react-router'
51+
import { routeTree } from '../routeTree.gen'
52+
import { queryClient } from './queryClient'
53+
54+
export const router = createRouter({
55+
routeTree,
56+
context: { queryClient },
57+
defaultPreload: 'intent',
58+
defaultPreloadStaleTime: 0,
59+
})
60+
61+
declare module '@tanstack/react-router' {
62+
interface Register {
63+
router: typeof router
64+
}
65+
}
66+
```
67+
68+
```tsx
69+
// src/main.tsx
70+
import { RouterProvider } from '@tanstack/react-router'
71+
import { QueryClientProvider } from '@tanstack/react-query'
72+
import { router } from './lib/router'
73+
import { queryClient } from './lib/queryClient'
74+
75+
ReactDOM.createRoot(document.getElementById('root')!).render(
76+
<QueryClientProvider client={queryClient}>
77+
<RouterProvider router={router} context={{ queryClient }} />
78+
</QueryClientProvider>
79+
)
80+
```
81+
82+
## Query Definitions (queryOptions factories)
83+
- Co-locate query key, fetcher, and staleTime in one place
84+
- Share between Router loaders and component hooks
85+
```ts
86+
// src/queries/posts.ts
87+
import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query'
88+
import { fetchPost, fetchPosts } from '../api/posts'
89+
90+
export const postKeys = {
91+
all: ['posts'] as const,
92+
lists: () => [...postKeys.all, 'list'] as const,
93+
list: (filters?: PostFilters) => [...postKeys.lists(), filters] as const,
94+
details: () => [...postKeys.all, 'detail'] as const,
95+
detail: (id: string) => [...postKeys.details(), id] as const,
96+
}
97+
98+
export const postDetailQueryOptions = (id: string) =>
99+
queryOptions({
100+
queryKey: postKeys.detail(id),
101+
queryFn: () => fetchPost(id),
102+
staleTime: 1000 * 60 * 5,
103+
})
104+
105+
export const postsListQueryOptions = (filters?: PostFilters) =>
106+
queryOptions({
107+
queryKey: postKeys.list(filters),
108+
queryFn: () => fetchPosts(filters),
109+
staleTime: 1000 * 60,
110+
})
111+
```
112+
113+
## Router Loader + Query Integration
114+
- Loaders call `queryClient.ensureQueryData` — populates cache, renders immediately without spinner
115+
- Components then call `useQuery` with the same options — reads from cache synchronously
116+
```tsx
117+
// src/routes/posts/$postId.tsx
118+
import { createFileRoute } from '@tanstack/react-router'
119+
import { useQuery } from '@tanstack/react-query'
120+
import { postDetailQueryOptions } from '../../queries/posts'
121+
122+
export const Route = createFileRoute('/posts/$postId')({
123+
loader: ({ context: { queryClient }, params }) =>
124+
queryClient.ensureQueryData(postDetailQueryOptions(params.postId)),
125+
126+
errorComponent: ({ error }) => <ErrorMessage error={error} />,
127+
pendingComponent: PostSkeleton,
128+
component: PostDetail,
129+
})
130+
131+
function PostDetail() {
132+
const { postId } = Route.useParams()
133+
// data is already in cache from loader — no loading state
134+
const { data: post } = useQuery(postDetailQueryOptions(postId))
135+
136+
return <article><h1>{post!.title}</h1></article>
137+
}
138+
```
139+
140+
## Search Params + Query Integration
141+
- Use TanStack Router search params as the source of truth for filter/pagination state
142+
- Pass search params into queryOptions to drive query key and fetcher
143+
```tsx
144+
// src/routes/posts/index.tsx
145+
import { createFileRoute, Link } from '@tanstack/react-router'
146+
import { useQuery } from '@tanstack/react-query'
147+
import { z } from 'zod'
148+
import { postsListQueryOptions } from '../../queries/posts'
149+
150+
const searchSchema = z.object({
151+
page: z.number().int().min(1).default(1),
152+
category: z.string().optional(),
153+
})
154+
155+
export const Route = createFileRoute('/posts/')({
156+
validateSearch: searchSchema,
157+
loader: ({ context: { queryClient }, location: { search } }) =>
158+
queryClient.ensureQueryData(postsListQueryOptions(search)),
159+
component: PostsList,
160+
})
161+
162+
function PostsList() {
163+
const search = Route.useSearch()
164+
const navigate = Route.useNavigate()
165+
const { data: posts } = useQuery(postsListQueryOptions(search))
166+
167+
return (
168+
<div>
169+
{posts?.map(post => (
170+
<Link key={post.id} to="/posts/$postId" params={{ postId: post.id }}>
171+
{post.title}
172+
</Link>
173+
))}
174+
<button onClick={() => navigate({ search: { ...search, page: search.page + 1 } })}>
175+
Next Page
176+
</button>
177+
</div>
178+
)
179+
}
180+
```
181+
182+
## Mutations
183+
```tsx
184+
import { useMutation, useQueryClient } from '@tanstack/react-query'
185+
import { useNavigate } from '@tanstack/react-router'
186+
import { postKeys } from '../../queries/posts'
187+
188+
function CreatePostForm() {
189+
const queryClient = useQueryClient()
190+
const navigate = useNavigate()
191+
192+
const mutation = useMutation({
193+
mutationFn: createPost,
194+
onSuccess: (newPost) => {
195+
// Populate detail cache immediately
196+
queryClient.setQueryData(postKeys.detail(newPost.id), newPost)
197+
// Invalidate list queries
198+
queryClient.invalidateQueries({ queryKey: postKeys.lists() })
199+
// Navigate to new post (no loading — cache is warm)
200+
navigate({ to: '/posts/$postId', params: { postId: newPost.id } })
201+
},
202+
})
203+
204+
return (/* form JSX */)
205+
}
206+
```
207+
208+
## Authentication Pattern
209+
```tsx
210+
// src/routes/__root.tsx
211+
import { createRootRouteWithContext } from '@tanstack/react-router'
212+
213+
export interface RouterContext {
214+
queryClient: QueryClient
215+
auth: { isAuthenticated: boolean; user: User | null }
216+
}
217+
218+
export const Route = createRootRouteWithContext<RouterContext>()({
219+
component: RootLayout,
220+
})
221+
222+
// src/routes/_auth.tsx (pathless layout for protected routes)
223+
export const Route = createFileRoute('/_auth')({
224+
beforeLoad: ({ context }) => {
225+
if (!context.auth.isAuthenticated) {
226+
throw redirect({ to: '/login', search: { redirect: location.pathname } })
227+
}
228+
},
229+
})
230+
```
231+
232+
## Prefetching on Hover
233+
```tsx
234+
function PostCard({ post }: { post: Post }) {
235+
const queryClient = useQueryClient()
236+
return (
237+
<Link
238+
to="/posts/$postId"
239+
params={{ postId: post.id }}
240+
onMouseEnter={() => queryClient.prefetchQuery(postDetailQueryOptions(post.id))}
241+
>
242+
{post.title}
243+
</Link>
244+
)
245+
}
246+
```
247+
248+
## DevTools (Development Only)
249+
```tsx
250+
// In __root.tsx
251+
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
252+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
253+
254+
// Inside component
255+
{import.meta.env.DEV && (
256+
<>
257+
<TanStackRouterDevtools position="bottom-left" />
258+
<ReactQueryDevtools buttonPosition="bottom-right" />
259+
</>
260+
)}
261+
```
262+
263+
## Key Rules
264+
- Always define `queryOptions` outside of components — not inline in `useQuery()`
265+
- Never use `useEffect` to fetch data — use loaders or `useQuery`
266+
- Always type router context — `declare module '@tanstack/react-router'` registration is required
267+
- Search params are the only source of truth for URL-driven filter state
268+
- Mutations should `setQueryData` + `invalidateQueries`, not just invalidate, for instant UI feedback

0 commit comments

Comments
 (0)