PluginBench
Rule

react tanstack router query

via PatrickJS/awesome-cursorrules

Zero-loading-spinner routing with TanStack Router v1 and Query v5 for type-safe React SPAs

What is react tanstack router query?

Combines TanStack Router for URL state and navigation with TanStack Query for server-state caching and async data management. Uses loaders to prefetch data into the Query cache before rendering, eliminating loading spinners at the route level while maintaining type safety throughout the application.

  • Prefetch route-level data via loaders with ensureQueryData to render immediately without spinners
  • Manage search params as the source of truth for filter and pagination state
  • Define reusable queryOptions factories that co-locate query keys, fetchers, and staleTime
  • Integrate mutations with optimistic updates using setQueryData and invalidateQueries for instant UI feedback
  • Set up type-safe router context with QueryClient and authentication state
  • Implement prefetching on hover and other intent-based preload strategies

Applies to

File patterns this rule matches.

**/*
Rule definition (reference)

Source of truth, from the repository.

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

// 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,
    },
  },
})
// 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
  }
}
// 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
// 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
// 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
// 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

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

// 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

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)

// 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

Related rules

Expert guidance for React, TypeScript, Next.js 14, and Web3 stack development with Wagmi and Viem.

**/*
41k
via PatrickJS/awesome-cursorrules

Full-stack React, TypeScript, and Symfony development assistant with Docker expertise.

**/*
41k
via PatrickJS/awesome-cursorrules

React and TypeScript state management guidance for Zustand stores, selectors, middleware, persistence, and testing.

**/*.ts +1
41k
via PatrickJS/awesome-cursorrules

Write READMEs that convert readers in 3-5 seconds with landing-page structure and working code examples.

**/*
41k
via PatrickJS/awesome-cursorrules

ROS and ROS2 best practices for packages, nodes, interfaces, timing, and testing.

["**/*.py" +11
41k
via PatrickJS/awesome-cursorrules

Build RTL-ready apps with logical CSS, Tailwind utilities, and automated auditing.

**/*
41k
via PatrickJS/awesome-cursorrules