{"filename":"react-tanstack-router-query-cursorrules-prompt-file.mdc","content":"---\ndescription: \"Cursor rules for React SPAs combining TanStack Router v1 and TanStack Query v5 for zero-loading-spinner routing and type-safe server state.\"\nglobs: **/*\nalwaysApply: false\n---\nYou are an expert in React, TanStack Router v1, TanStack Query v5, TypeScript, Vite, and building fully type-safe single-page applications.\n\n# React + TanStack Router + TanStack Query Guidelines\n\n## Architecture Overview\n- TanStack Router handles all routing, URL state, and navigation\n- TanStack Query manages all server state, caching, and async data\n- React components are pure UI — they read from Query cache and trigger mutations\n- Loaders bridge Router and Query: they prefetch into the Query cache before render\n- This eliminates loading spinners for route-level data; Suspense handles component-level loading\n\n## Project Setup\n```\nsrc/\n  routes/\n    __root.tsx\n    index.tsx\n    posts/\n      index.tsx\n      $postId.tsx\n  queries/            ← Query definitions (queryOptions factories)\n    posts.ts\n    users.ts\n  api/                ← API client functions (fetchers)\n    posts.ts\n    users.ts\n  lib/\n    queryClient.ts\n    router.ts\n  main.tsx\n```\n\n## QueryClient + Router Setup\n```ts\n// src/lib/queryClient.ts\nimport { QueryClient } from '@tanstack/react-query'\n\nexport const queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      staleTime: 1000 * 60,\n      retry: (count, error: any) => error?.status !== 404 && count < 2,\n    },\n  },\n})\n```\n\n```tsx\n// src/lib/router.ts\nimport { createRouter } from '@tanstack/react-router'\nimport { routeTree } from '../routeTree.gen'\nimport { queryClient } from './queryClient'\n\nexport const router = createRouter({\n  routeTree,\n  context: { queryClient },\n  defaultPreload: 'intent',\n  defaultPreloadStaleTime: 0,\n})\n\ndeclare module '@tanstack/react-router' {\n  interface Register {\n    router: typeof router\n  }\n}\n```\n\n```tsx\n// src/main.tsx\nimport { RouterProvider } from '@tanstack/react-router'\nimport { QueryClientProvider } from '@tanstack/react-query'\nimport { router } from './lib/router'\nimport { queryClient } from './lib/queryClient'\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n  <QueryClientProvider client={queryClient}>\n    <RouterProvider router={router} context={{ queryClient }} />\n  </QueryClientProvider>\n)\n```\n\n## Query Definitions (queryOptions factories)\n- Co-locate query key, fetcher, and staleTime in one place\n- Share between Router loaders and component hooks\n```ts\n// src/queries/posts.ts\nimport { queryOptions, infiniteQueryOptions } from '@tanstack/react-query'\nimport { fetchPost, fetchPosts } from '../api/posts'\n\nexport const postKeys = {\n  all: ['posts'] as const,\n  lists: () => [...postKeys.all, 'list'] as const,\n  list: (filters?: PostFilters) => [...postKeys.lists(), filters] as const,\n  details: () => [...postKeys.all, 'detail'] as const,\n  detail: (id: string) => [...postKeys.details(), id] as const,\n}\n\nexport const postDetailQueryOptions = (id: string) =>\n  queryOptions({\n    queryKey: postKeys.detail(id),\n    queryFn: () => fetchPost(id),\n    staleTime: 1000 * 60 * 5,\n  })\n\nexport const postsListQueryOptions = (filters?: PostFilters) =>\n  queryOptions({\n    queryKey: postKeys.list(filters),\n    queryFn: () => fetchPosts(filters),\n    staleTime: 1000 * 60,\n  })\n```\n\n## Router Loader + Query Integration\n- Loaders call `queryClient.ensureQueryData` — populates cache, renders immediately without spinner\n- Components then call `useQuery` with the same options — reads from cache synchronously\n```tsx\n// src/routes/posts/$postId.tsx\nimport { createFileRoute } from '@tanstack/react-router'\nimport { useQuery } from '@tanstack/react-query'\nimport { postDetailQueryOptions } from '../../queries/posts'\n\nexport const Route = createFileRoute('/posts/$postId')({\n  loader: ({ context: { queryClient }, params }) =>\n    queryClient.ensureQueryData(postDetailQueryOptions(params.postId)),\n\n  errorComponent: ({ error }) => <ErrorMessage error={error} />,\n  pendingComponent: PostSkeleton,\n  component: PostDetail,\n})\n\nfunction PostDetail() {\n  const { postId } = Route.useParams()\n  // data is already in cache from loader — no loading state\n  const { data: post } = useQuery(postDetailQueryOptions(postId))\n\n  return <article><h1>{post!.title}</h1></article>\n}\n```\n\n## Search Params + Query Integration\n- Use TanStack Router search params as the source of truth for filter/pagination state\n- Pass search params into queryOptions to drive query key and fetcher\n```tsx\n// src/routes/posts/index.tsx\nimport { createFileRoute, Link } from '@tanstack/react-router'\nimport { useQuery } from '@tanstack/react-query'\nimport { z } from 'zod'\nimport { postsListQueryOptions } from '../../queries/posts'\n\nconst searchSchema = z.object({\n  page: z.number().int().min(1).default(1),\n  category: z.string().optional(),\n})\n\nexport const Route = createFileRoute('/posts/')({\n  validateSearch: searchSchema,\n  loader: ({ context: { queryClient }, location: { search } }) =>\n    queryClient.ensureQueryData(postsListQueryOptions(search)),\n  component: PostsList,\n})\n\nfunction PostsList() {\n  const search = Route.useSearch()\n  const navigate = Route.useNavigate()\n  const { data: posts } = useQuery(postsListQueryOptions(search))\n\n  return (\n    <div>\n      {posts?.map(post => (\n        <Link key={post.id} to=\"/posts/$postId\" params={{ postId: post.id }}>\n          {post.title}\n        </Link>\n      ))}\n      <button onClick={() => navigate({ search: { ...search, page: search.page + 1 } })}>\n        Next Page\n      </button>\n    </div>\n  )\n}\n```\n\n## Mutations\n```tsx\nimport { useMutation, useQueryClient } from '@tanstack/react-query'\nimport { useNavigate } from '@tanstack/react-router'\nimport { postKeys } from '../../queries/posts'\n\nfunction CreatePostForm() {\n  const queryClient = useQueryClient()\n  const navigate = useNavigate()\n\n  const mutation = useMutation({\n    mutationFn: createPost,\n    onSuccess: (newPost) => {\n      // Populate detail cache immediately\n      queryClient.setQueryData(postKeys.detail(newPost.id), newPost)\n      // Invalidate list queries\n      queryClient.invalidateQueries({ queryKey: postKeys.lists() })\n      // Navigate to new post (no loading — cache is warm)\n      navigate({ to: '/posts/$postId', params: { postId: newPost.id } })\n    },\n  })\n\n  return (/* form JSX */)\n}\n```\n\n## Authentication Pattern\n```tsx\n// src/routes/__root.tsx\nimport { createRootRouteWithContext } from '@tanstack/react-router'\n\nexport interface RouterContext {\n  queryClient: QueryClient\n  auth: { isAuthenticated: boolean; user: User | null }\n}\n\nexport const Route = createRootRouteWithContext<RouterContext>()({\n  component: RootLayout,\n})\n\n// src/routes/_auth.tsx (pathless layout for protected routes)\nexport const Route = createFileRoute('/_auth')({\n  beforeLoad: ({ context }) => {\n    if (!context.auth.isAuthenticated) {\n      throw redirect({ to: '/login', search: { redirect: location.pathname } })\n    }\n  },\n})\n```\n\n## Prefetching on Hover\n```tsx\nfunction PostCard({ post }: { post: Post }) {\n  const queryClient = useQueryClient()\n  return (\n    <Link\n      to=\"/posts/$postId\"\n      params={{ postId: post.id }}\n      onMouseEnter={() => queryClient.prefetchQuery(postDetailQueryOptions(post.id))}\n    >\n      {post.title}\n    </Link>\n  )\n}\n```\n\n## DevTools (Development Only)\n```tsx\n// In __root.tsx\nimport { TanStackRouterDevtools } from '@tanstack/router-devtools'\nimport { ReactQueryDevtools } from '@tanstack/react-query-devtools'\n\n// Inside component\n{import.meta.env.DEV && (\n  <>\n    <TanStackRouterDevtools position=\"bottom-left\" />\n    <ReactQueryDevtools buttonPosition=\"bottom-right\" />\n  </>\n)}\n```\n\n## Key Rules\n- Always define `queryOptions` outside of components — not inline in `useQuery()`\n- Never use `useEffect` to fetch data — use loaders or `useQuery`\n- Always type router context — `declare module '@tanstack/react-router'` registration is required\n- Search params are the only source of truth for URL-driven filter state\n- Mutations should `setQueryData` + `invalidateQueries`, not just invalidate, for instant UI feedback\n"}