cursor / claude rules

React TanStack Router + Query Cursor Rules

Cursor rules for React SPAs combining TanStack Router v1 and TanStack Query v5. Loaders prefetch into Query, no loading spinners for route data.
by cache.directory updated Sep 10, 2026 CC-BY-4.0

## what it does

tl;dr: paste this .mdc into .cursor/rules/ and Cursor will stop suggesting loading spinners, ad-hoc fetch calls in components, or duplicate cache layers between Router and Query.

What the upstream rule covers

Sourced from PatrickJS/awesome-cursorrules → react-tanstack-router-query-cursorrules-prompt-file.mdc. The rule is the cleanest public write-up of how TanStack Router v1 loaders should prefetch into the TanStack Query v5 cache instead of fetching independently.

The architectural claim: Route-level data lives in Query. Loaders run before render and call queryClient.ensureQueryData(...). Components read from Query hooks. The result is no loading spinner on navigation — Suspense handles component-level loading only.

What it tells Cursor

  • Architecture: Router owns routing, URL state, navigation; Query owns server state, caching, async data; components are pure UI reading from the Query cache; loaders bridge the two via ensureQueryData.
  • Layout:
    src/
      routes/         # __root.tsx, index.tsx, posts/$postId.tsx
      queries/        # queryOptions factories (posts.ts, users.ts)
      api/            # fetchers (posts.ts, users.ts)
      lib/            # queryClient.ts, router.ts
      main.tsx
  • Query definitions: co-locate query key, fetcher, and staleTime in queryOptions factories; share between Router loaders and component hooks; use infiniteQueryOptions for paged lists.
  • Routing patterns: typed routes via createRouter({ routeTree, context: { queryClient } }); defaultPreload: "intent" for hover/prefetch; defaultPreloadStaleTime: 0; Register module augmentation so route params, search validation, and loaders are typed end-to-end.
  • Mutations: invalidate by prefix (queryClient.invalidateQueries({ queryKey: postKeys.lists() })); colocate onMutate / onError / onSettled so optimistic updates roll back correctly.
  • Auth context: ship the current user (or auth claims) on the Router context, fetch once at the root loader, and pass it down through beforeLoad guards rather than re-querying per route.

Install

mkdir -p .cursor/rules
curl -L -o .cursor/rules/react-tanstack-router-query.mdc \
  https://raw.githubusercontent.com/PatrickJS/awesome-cursorrules/main/rules/react-tanstack-router-query-cursorrules-prompt-file.mdc

Bootstrap the rule expects

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

Gotchas

  • The retry: (count, error) => error?.status !== 404 && count < 2 line is load-bearing: 404s on missing entities are not retryable and shouldn’t burn the retry budget. Don’t drop the status check.
  • defaultPreloadStaleTime: 0 pairs with queryClient’s own staleTime — preloaded data is revalidated on mount rather than served stale.
  • The upstream frontmatter is globs: **/*, alwaysApply: false. The **/* is intentional: route files live anywhere. Do not narrow it.
  • Use this only if you’re on TanStack Router v1. The older @tanstack/router v0 package has a different loader API.

## embed this badge

cache ✓ in cache.directory
![cached](https://cache.directory/badge/react-tanstack-router-query.svg)