diff --git a/README.md b/README.md
index 63c6a2ff..3e975eb1 100644
--- a/README.md
+++ b/README.md
@@ -181,6 +181,7 @@ By creating a `.cursorrules` file in your project's root directory, you can leve
- [React (Redux, TypeScript)](./rules/react-redux-typescript-cursorrules-prompt-file/.cursorrules) - Cursor rules for React development with Redux and TypeScript integration.
- [React (MobX)](./rules/react-mobx-cursorrules-prompt-file/.cursorrules) - Cursor rules for React development with MobX integration.
- [React (React Query)](./rules/react-query-cursorrules-prompt-file/.cursorrules) - Cursor rules for React development with React Query integration.
+- [TanStack Query v5](/rules/tanstack-query-v5-cursorrules-prompt-file/.cursorrules) - Cursor rules for TanStack Query v5 including queryOptions helper, query key factories, mutations, optimistic updates, infinite queries, and Suspense mode.
### Database and API
diff --git a/rules-new/tanstack-query.mdc b/rules-new/tanstack-query.mdc
new file mode 100644
index 00000000..b1539325
--- /dev/null
+++ b/rules-new/tanstack-query.mdc
@@ -0,0 +1,108 @@
+---
+description: TanStack Query v5 (React Query) patterns including queryOptions helper, query key factories, mutations, optimistic updates, infinite queries, Suspense mode, and prefetching
+globs: ["src/**/*.tsx", "src/**/*.ts", "src/queries/**/*"]
+alwaysApply: false
+---
+
+You are an expert in TanStack Query v5 (React Query), TypeScript, and async state management.
+
+## Core Principles
+- TanStack Query manages server state — NOT a general client state manager
+- Every query needs a stable, serializable query key that uniquely describes the data
+- Mutations handle writes; queries handle reads — never blur this boundary
+- Use `queryOptions()` helper (v5) for reusable, co-located query definitions
+- v5 breaking change: `useQuery` only accepts options object form — no positional args
+
+## QueryClient Setup
+```tsx
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 1000 * 60,
+ retry: (count, error: any) => error?.status !== 404 && count < 2,
+ },
+ },
+})
+```
+
+## Query Key Factory Pattern
+```ts
+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,
+}
+```
+
+## queryOptions Helper (v5)
+```ts
+export const postQueryOptions = (id: string) =>
+ queryOptions({
+ queryKey: postKeys.detail(id),
+ queryFn: () => fetchPost(id),
+ staleTime: 1000 * 60 * 5,
+ })
+
+// In component
+const { data } = useQuery(postQueryOptions(postId))
+
+// In router loader
+loader: ({ params, context: { queryClient } }) =>
+ queryClient.ensureQueryData(postQueryOptions(params.postId))
+```
+
+## Mutations
+```tsx
+const { mutate, isPending } = useMutation({
+ mutationFn: (input: CreatePostInput) => createPost(input),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: postKeys.lists() })
+ },
+ onError: (error) => toast.error(error.message),
+})
+```
+
+## Optimistic Updates
+```tsx
+const mutation = useMutation({
+ mutationFn: updatePost,
+ onMutate: async (updated) => {
+ await queryClient.cancelQueries({ queryKey: postKeys.detail(updated.id) })
+ const previous = queryClient.getQueryData(postKeys.detail(updated.id))
+ queryClient.setQueryData(postKeys.detail(updated.id), updated)
+ return { previous }
+ },
+ onError: (_, updated, ctx) => {
+ queryClient.setQueryData(postKeys.detail(updated.id), ctx?.previous)
+ },
+ onSettled: (_, __, updated) => {
+ queryClient.invalidateQueries({ queryKey: postKeys.detail(updated.id) })
+ },
+})
+```
+
+## Infinite Queries
+```tsx
+const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
+ queryKey: postKeys.lists(),
+ queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam }),
+ initialPageParam: undefined as string | undefined,
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
+})
+const allPosts = data?.pages.flatMap((p) => p.items) ?? []
+```
+
+## Suspense Mode (v5)
+```tsx
+// useSuspenseQuery — no isLoading needed, Suspense handles it
+const { data } = useSuspenseQuery(postQueryOptions(postId))
+// Wrap with }> +
+```
+
+## Key Rules
+- Always define `queryOptions` outside components — never inline in `useQuery()`
+- Never use `useEffect` to fetch data — use loaders or `useQuery`
+- Use `placeholderData: keepPreviousData` for pagination to avoid layout shifts
+- Instantiate `QueryClient` once at app root — never inside a component
diff --git a/rules/tanstack-query-v5-cursorrules-prompt-file/.cursorrules b/rules/tanstack-query-v5-cursorrules-prompt-file/.cursorrules
new file mode 100644
index 00000000..a4c7ddca
--- /dev/null
+++ b/rules/tanstack-query-v5-cursorrules-prompt-file/.cursorrules
@@ -0,0 +1,204 @@
+You are an expert in TanStack Query v5 (formerly React Query), TypeScript, and async state management for React applications.
+
+# TanStack Query v5 Guidelines
+
+## Core Philosophy
+- TanStack Query manages server state — it is NOT a general state manager for client-only state
+- Every query should have a stable, serializable query key that uniquely describes the data
+- Mutations handle writes; queries handle reads — never blur this boundary
+- Prefer `queryOptions()` helper for reusable, co-located query definitions
+- v5 breaking changes: `useQuery` no longer accepts positional args; always use the options object form
+
+## Setup
+```tsx
+// main.tsx
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 1000 * 60, // 1 minute default stale time
+ retry: 2,
+ refetchOnWindowFocus: true,
+ },
+ },
+})
+
+function App() {
+ return (
+
+
+
+
+ )
+}
+```
+
+## Query Keys
+- Always structure keys as arrays: `['entity', 'list']`, `['entity', 'detail', id]`
+- Use a query key factory to avoid typos and enable easy invalidation
+```ts
+// queryKeys.ts
+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,
+}
+```
+
+## queryOptions Helper (v5)
+- Use `queryOptions()` to define queries once and reuse across components and loaders
+```ts
+import { queryOptions } from '@tanstack/react-query'
+
+export const postQueryOptions = (id: string) =>
+ queryOptions({
+ queryKey: postKeys.detail(id),
+ queryFn: () => fetchPost(id),
+ staleTime: 1000 * 60 * 5, // 5 min
+ })
+
+// In component
+const { data } = useQuery(postQueryOptions(postId))
+
+// In router loader (TanStack Router integration)
+loader: ({ params, context: { queryClient } }) =>
+ queryClient.ensureQueryData(postQueryOptions(params.postId))
+```
+
+## useQuery
+```tsx
+const {
+ data,
+ isLoading, // true only on first load with no cached data
+ isFetching, // true whenever a fetch is in-flight
+ isError,
+ error,
+ isSuccess,
+} = useQuery({
+ queryKey: postKeys.detail(postId),
+ queryFn: () => fetchPost(postId),
+ enabled: !!postId, // disable query if params not ready
+})
+```
+
+## useMutation
+```tsx
+const { mutate, mutateAsync, isPending } = useMutation({
+ mutationFn: (newPost: CreatePostInput) => createPost(newPost),
+ onSuccess: (data) => {
+ // Invalidate and refetch
+ queryClient.invalidateQueries({ queryKey: postKeys.lists() })
+ toast.success('Post created!')
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+})
+
+// Usage
+mutate({ title: 'Hello', body: '...' })
+```
+
+## Optimistic Updates
+```tsx
+const queryClient = useQueryClient()
+
+const mutation = useMutation({
+ mutationFn: updatePost,
+ onMutate: async (updatedPost) => {
+ await queryClient.cancelQueries({ queryKey: postKeys.detail(updatedPost.id) })
+ const previous = queryClient.getQueryData(postKeys.detail(updatedPost.id))
+ queryClient.setQueryData(postKeys.detail(updatedPost.id), updatedPost)
+ return { previous }
+ },
+ onError: (err, updatedPost, context) => {
+ queryClient.setQueryData(postKeys.detail(updatedPost.id), context?.previous)
+ },
+ onSettled: (_, __, updatedPost) => {
+ queryClient.invalidateQueries({ queryKey: postKeys.detail(updatedPost.id) })
+ },
+})
+```
+
+## Infinite Queries
+```tsx
+const {
+ data,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+} = useInfiniteQuery({
+ queryKey: postKeys.lists(),
+ queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam }),
+ initialPageParam: undefined as string | undefined,
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
+})
+
+// data.pages is an array of page results — flatten for rendering
+const allPosts = data?.pages.flatMap((page) => page.items) ?? []
+```
+
+## Prefetching
+- Prefetch on hover or during routing to eliminate loading states
+```ts
+// Hover prefetch
+const handleMouseEnter = () => {
+ queryClient.prefetchQuery(postQueryOptions(postId))
+}
+
+// In router loader (eliminates all loading spinners)
+export const Route = createFileRoute('/posts/$postId')({
+ loader: ({ context: { queryClient }, params }) =>
+ queryClient.ensureQueryData(postQueryOptions(params.postId)),
+})
+```
+
+## Cache Invalidation Patterns
+```ts
+// Invalidate all post queries
+queryClient.invalidateQueries({ queryKey: postKeys.all })
+
+// Invalidate only post lists
+queryClient.invalidateQueries({ queryKey: postKeys.lists() })
+
+// Remove from cache entirely
+queryClient.removeQueries({ queryKey: postKeys.detail(id) })
+
+// Directly update cache without refetch
+queryClient.setQueryData(postKeys.detail(id), newData)
+```
+
+## Suspense Mode
+- Use `useSuspenseQuery` for Suspense-based data fetching (v5)
+- Wrap with `}>`
+- Pair with `` for error handling
+```tsx
+// No need to handle isLoading — Suspense handles it
+const { data } = useSuspenseQuery(postQueryOptions(postId))
+```
+
+## Performance Best Practices
+- Set appropriate `staleTime` per query — defaults to `0` (always stale)
+- Use `select` to transform/subscribe to only relevant slices of data
+- Use `placeholderData: keepPreviousData` for pagination to avoid layout shifts
+- Avoid creating `QueryClient` inside components — instantiate once at app root
+- Use `notifyOnChangeProps` to limit re-renders to only relevant data changes
+
+## Error Handling
+- Use `throwOnError: true` to bubble errors to the nearest ErrorBoundary
+- Use `retry` function for conditional retry logic (e.g., skip retry on 404)
+```ts
+retry: (failureCount, error) => {
+ if (error.status === 404) return false
+ return failureCount < 3
+},
+```
+
+## TypeScript Tips
+- Always type `queryFn` return value explicitly or infer from typed API functions
+- Use `QueryObserverResult` to type hook return values
+- Use `UseMutationResult` for mutations
diff --git a/rules/tanstack-query-v5-cursorrules-prompt-file/README.md b/rules/tanstack-query-v5-cursorrules-prompt-file/README.md
new file mode 100644
index 00000000..2e54a1df
--- /dev/null
+++ b/rules/tanstack-query-v5-cursorrules-prompt-file/README.md
@@ -0,0 +1,19 @@
+# TanStack Query v5 Cursor Rules
+
+Cursor rules for TanStack Query v5 (React Query), covering the v5 API changes, `queryOptions` helper, query key factories, mutations, optimistic updates, infinite queries, Suspense mode, prefetching, and cache management.
+
+## What's covered
+- QueryClient setup with sensible defaults
+- Query key factory pattern
+- `queryOptions()` helper for reusable query definitions
+- `useQuery`, `useMutation`, `useInfiniteQuery` patterns
+- Optimistic updates with rollback
+- Suspense mode with `useSuspenseQuery`
+- Prefetching and TanStack Router loader integration
+- Cache invalidation strategies
+- TypeScript best practices
+
+## Author
+Created by [usm4nhafeez](https://github.com/usm4nhafeez)
+
+Contributed to [awesome-cursorrules](https://github.com/PatrickJS/awesome-cursorrules)
diff --git a/rules/tanstack-query-v5-cursorrules-prompt-file/tanstack-query.mdc b/rules/tanstack-query-v5-cursorrules-prompt-file/tanstack-query.mdc
new file mode 100644
index 00000000..b1539325
--- /dev/null
+++ b/rules/tanstack-query-v5-cursorrules-prompt-file/tanstack-query.mdc
@@ -0,0 +1,108 @@
+---
+description: TanStack Query v5 (React Query) patterns including queryOptions helper, query key factories, mutations, optimistic updates, infinite queries, Suspense mode, and prefetching
+globs: ["src/**/*.tsx", "src/**/*.ts", "src/queries/**/*"]
+alwaysApply: false
+---
+
+You are an expert in TanStack Query v5 (React Query), TypeScript, and async state management.
+
+## Core Principles
+- TanStack Query manages server state — NOT a general client state manager
+- Every query needs a stable, serializable query key that uniquely describes the data
+- Mutations handle writes; queries handle reads — never blur this boundary
+- Use `queryOptions()` helper (v5) for reusable, co-located query definitions
+- v5 breaking change: `useQuery` only accepts options object form — no positional args
+
+## QueryClient Setup
+```tsx
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 1000 * 60,
+ retry: (count, error: any) => error?.status !== 404 && count < 2,
+ },
+ },
+})
+```
+
+## Query Key Factory Pattern
+```ts
+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,
+}
+```
+
+## queryOptions Helper (v5)
+```ts
+export const postQueryOptions = (id: string) =>
+ queryOptions({
+ queryKey: postKeys.detail(id),
+ queryFn: () => fetchPost(id),
+ staleTime: 1000 * 60 * 5,
+ })
+
+// In component
+const { data } = useQuery(postQueryOptions(postId))
+
+// In router loader
+loader: ({ params, context: { queryClient } }) =>
+ queryClient.ensureQueryData(postQueryOptions(params.postId))
+```
+
+## Mutations
+```tsx
+const { mutate, isPending } = useMutation({
+ mutationFn: (input: CreatePostInput) => createPost(input),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: postKeys.lists() })
+ },
+ onError: (error) => toast.error(error.message),
+})
+```
+
+## Optimistic Updates
+```tsx
+const mutation = useMutation({
+ mutationFn: updatePost,
+ onMutate: async (updated) => {
+ await queryClient.cancelQueries({ queryKey: postKeys.detail(updated.id) })
+ const previous = queryClient.getQueryData(postKeys.detail(updated.id))
+ queryClient.setQueryData(postKeys.detail(updated.id), updated)
+ return { previous }
+ },
+ onError: (_, updated, ctx) => {
+ queryClient.setQueryData(postKeys.detail(updated.id), ctx?.previous)
+ },
+ onSettled: (_, __, updated) => {
+ queryClient.invalidateQueries({ queryKey: postKeys.detail(updated.id) })
+ },
+})
+```
+
+## Infinite Queries
+```tsx
+const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
+ queryKey: postKeys.lists(),
+ queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam }),
+ initialPageParam: undefined as string | undefined,
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
+})
+const allPosts = data?.pages.flatMap((p) => p.items) ?? []
+```
+
+## Suspense Mode (v5)
+```tsx
+// useSuspenseQuery — no isLoading needed, Suspense handles it
+const { data } = useSuspenseQuery(postQueryOptions(postId))
+// Wrap with }> +
+```
+
+## Key Rules
+- Always define `queryOptions` outside components — never inline in `useQuery()`
+- Never use `useEffect` to fetch data — use loaders or `useQuery`
+- Use `placeholderData: keepPreviousData` for pagination to avoid layout shifts
+- Instantiate `QueryClient` once at app root — never inside a component