From 5b8465ad72ad84bb191e4752d6f3ac60a4593060 Mon Sep 17 00:00:00 2001 From: usm4nhafeez Date: Mon, 13 Apr 2026 12:17:24 +0500 Subject: [PATCH] feat: adding TanStack Query v5 cursorrules --- README.md | 1 + rules-new/tanstack-query.mdc | 108 ++++++++++ .../.cursorrules | 204 ++++++++++++++++++ .../README.md | 19 ++ .../tanstack-query.mdc | 108 ++++++++++ 5 files changed, 440 insertions(+) create mode 100644 rules-new/tanstack-query.mdc create mode 100644 rules/tanstack-query-v5-cursorrules-prompt-file/.cursorrules create mode 100644 rules/tanstack-query-v5-cursorrules-prompt-file/README.md create mode 100644 rules/tanstack-query-v5-cursorrules-prompt-file/tanstack-query.mdc 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