{"filename":"react-tanstack-router-query.mdc","content":"---\ndescription: \"React SPA with TanStack Router v1 + TanStack Query v5 — the definitive pattern for zero-loading-spinner routing, type-safe URLs, and cache-first data\"\nglobs: [\"src/routes/**/*\", \"src/queries/**/*\", \"src/lib/router.ts\", \"src/lib/queryClient.ts\"]\nalwaysApply: false\n---\nYou are an expert in React, TanStack Router v1, TanStack Query v5, TypeScript, and Vite.\n\n## Architecture\n- TanStack Router: routing, URL state, navigation\n- TanStack Query: server state, caching, mutations\n- Loader = bridge: prefetches into Query cache before render → zero loading spinners for route data\n- Components are pure UI: read from Query cache, trigger mutations\n\n## Setup\n```ts\n// src/lib/queryClient.ts\nexport const queryClient = new QueryClient({\n  defaultOptions: { queries: { staleTime: 60_000 } },\n})\n\n// src/lib/router.ts\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 { router: typeof router }\n}\n\n// src/main.tsx\n<QueryClientProvider client={queryClient}>\n  <RouterProvider router={router} context={{ queryClient }} />\n</QueryClientProvider>\n```\n\n## Query Definitions\n```ts\n// src/queries/posts.ts\nexport const postKeys = {\n  all: ['posts'] as const,\n  detail: (id: string) => [...postKeys.all, 'detail', id] as const,\n  list: (f?: PostFilters) => [...postKeys.all, 'list', f] as const,\n}\n\nexport const postQueryOptions = (id: string) =>\n  queryOptions({ queryKey: postKeys.detail(id), queryFn: () => fetchPost(id) })\n\nexport const postsQueryOptions = (filters?: PostFilters) =>\n  queryOptions({ queryKey: postKeys.list(filters), queryFn: () => fetchPosts(filters) })\n```\n\n## Loader + Component (zero loading state)\n```tsx\nexport const Route = createFileRoute('/posts/$postId')({\n  loader: ({ context: { queryClient }, params }) =>\n    queryClient.ensureQueryData(postQueryOptions(params.postId)),\n  component: PostDetail,\n})\n\nfunction PostDetail() {\n  const { postId } = Route.useParams()\n  const { data: post } = useQuery(postQueryOptions(postId))  // always in cache from loader\n  return <h1>{post!.title}</h1>\n}\n```\n\n## Search Params → Query Key\n```tsx\nconst searchSchema = z.object({ page: z.number().default(1), q: z.string().optional() })\n\nexport const Route = createFileRoute('/posts/')({\n  validateSearch: searchSchema,\n  loader: ({ context: { queryClient }, location: { search } }) =>\n    queryClient.ensureQueryData(postsQueryOptions(search)),\n  component: PostsList,\n})\n\nfunction PostsList() {\n  const search = Route.useSearch()\n  const { data } = useQuery(postsQueryOptions(search))\n  // ...\n}\n```\n\n## Mutations\n```tsx\nconst mutation = useMutation({\n  mutationFn: createPost,\n  onSuccess: (newPost) => {\n    queryClient.setQueryData(postKeys.detail(newPost.id), newPost)  // warm cache\n    queryClient.invalidateQueries({ queryKey: postKeys.list() })\n    navigate({ to: '/posts/$postId', params: { postId: newPost.id } })  // instant — no spinner\n  },\n})\n```\n\n## Hover Prefetching\n```tsx\n<Link\n  to=\"/posts/$postId\"\n  params={{ postId: post.id }}\n  onMouseEnter={() => queryClient.prefetchQuery(postQueryOptions(post.id))}\n>\n  {post.title}\n</Link>\n```\n\n## Key Rules\n- Always define `queryOptions` outside components — never inline inside `useQuery()`\n- Never use `useEffect` for data fetching — use loaders or `useQuery`\n- Search params are the single source of truth for filter/pagination state\n- After mutations: `setQueryData` + `invalidateQueries` for instant UI feedback\n- `declare module '@tanstack/react-router'` router registration is required for full type safety\n"}