Home › All rules › Community › React Tanstack Router Query Cursor Rules

React Tanstack Router Query Cursor Rules

Cursor rules for React SPAs combining TanStack Router v1 and TanStack Query v5 for zero-loading-spinner routing and type-safe server state.

CursorReactCommunity (CC0)

How to use

Save this as .cursor/rules/react-tanstack-router-query-cursorrules-prompt-file.mdc in your project, commit it, and Cursor will load it. Review it first: community files can be opinionated or out of date.

Note: Review and test before use. Provided as is, without warranty. See Terms.

.cursor/rules/react-tanstack-router-query-cursorrules-prompt-file.mdcDownload  
---
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

More: React rules · all community rules

Related rules

React + TypeScript Copilot path instructions

Path-specific Copilot instructions for React 19 + TypeScript .tsx files: components, hooks, accessibility, and tests.

GitHub CopilotReact

React 19 + Vite SPA rules

Cursor rules for React 19 single-page apps on Vite 6+ with TypeScript, hooks, and Vitest.

CursorReact

TanStack Query v5 rules

Cursor rules for TanStack Query v5 in React: query keys, queryOptions, mutations, and cache invalidation.

CursorReact

Cursor AI React TypeScript Shadcn Ui Cursor Rules

Cursor rules for Cursor AI development with React, TypeScript, and shadcn/ui integration.

CursorReactCommunity (CC0)

React Chakra Ui Cursor Rules

Cursor rules for React development with Chakra UI integration.

CursorReactCommunity (CC0)

React Components Creation Cursor Rules

Cursor rules for React component creation and development.

CursorReactCommunity (CC0)

Get new rules weekly

One email a week: fresh rule files, agent tips, and what's changing in Cursor, Claude Code and Copilot. Free, unsubscribe anytime.

Opens the Rulestack Newsletter on beehiiv in a new tab. Or subscribe directly on beehiiv.