diff --git a/_artifacts/domain_map.yaml b/_artifacts/domain_map.yaml new file mode 100644 index 00000000000..1f3c86396fa --- /dev/null +++ b/_artifacts/domain_map.yaml @@ -0,0 +1,2180 @@ +# domain_map.yaml +# Generated by skill-domain-discovery +# Library: TanStack Query +# Version: 5.101.0 / Svelte and Vue devtools 6.1.34 / Lit 0.2.7 +# Date: 2026-06-03 +# Status: draft + +library: + name: 'TanStack Query' + version: '5.101.0' + repository: 'https://github.com/TanStack/query' + docs: 'https://tanstack.com/query' + description: 'Framework adapters and core utilities for fetching, caching, synchronizing, and updating server state.' + primary_framework: 'framework-agnostic core with React-first docs and adapters for React, Preact, Vue, Solid, Svelte, Angular, and Lit' + discovery_mode: 'Maintainer skipped live interviews; docs guide order and TKDodo blog order are treated as priority signals.' + docs_read: + root_markdown_files: 493 + docs_config: 'docs/config.json' + priority_sources: + - 'Guides & Concepts order in docs/config.json' + - 'TKDodo blog all-post order at https://tkdodo.eu/blog/all' + client_facing_packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/react-query-devtools' + - '@tanstack/react-query-next-experimental' + - '@tanstack/react-query-persist-client' + - '@tanstack/preact-query' + - '@tanstack/preact-query-devtools' + - '@tanstack/preact-query-persist-client' + - '@tanstack/vue-query' + - '@tanstack/vue-query-devtools' + - '@tanstack/solid-query' + - '@tanstack/solid-query-devtools' + - '@tanstack/solid-query-persist-client' + - '@tanstack/svelte-query' + - '@tanstack/svelte-query-devtools' + - '@tanstack/svelte-query-persist-client' + - '@tanstack/angular-query-experimental' + - '@tanstack/lit-query' + - '@tanstack/query-persist-client-core' + - '@tanstack/query-async-storage-persister' + - '@tanstack/query-sync-storage-persister' + - '@tanstack/query-broadcast-client-experimental' + - '@tanstack/eslint-plugin-query' + - '@tanstack/query-codemods' + ecosystem_composition_targets: + - 'TanStack Router' + - 'TanStack Start' + - '@tanstack/react-router' + - '@tanstack/react-router-ssr-query' + - '@tanstack/react-start' + - '@tanstack/solid-start' + +domains: + - name: 'Bootstrapping query clients' + slug: 'bootstrapping-query-clients' + description: 'Creating stable QueryClient instances, wiring providers, and respecting framework lifecycle boundaries.' + - name: 'Reading server state' + slug: 'reading-server-state' + description: 'Declaring queries, choosing keys and options, handling status, fetchStatus, stale data, and query functions.' + - name: 'Coordinating query execution' + slug: 'coordinating-query-execution' + description: 'Managing dependent, parallel, disabled, paused, retried, polled, prefetched, paginated, and infinite reads.' + - name: 'Writing server state' + slug: 'writing-server-state' + description: 'Using mutations, invalidation, direct cache writes, optimistic updates, rollback, and cancellation.' + - name: 'Shaping cache and render behavior' + slug: 'shaping-cache-and-render-behavior' + description: 'Using initial data, placeholder data, selectors, structural sharing, tracked props, and typed reusable options.' + - name: 'Rendering across environments' + slug: 'rendering-across-environments' + description: 'Using prefetch, TanStack Router loaders, TanStack Start, dehydrate, hydrate, Suspense, streaming, SSR, RSC, SvelteKit, Nuxt, SolidStart, and Lit SSR correctly.' + - name: 'Persisting and synchronizing caches' + slug: 'persisting-and-synchronizing-caches' + description: 'Persisting query and mutation caches, restoring safely, offline behavior, storage persisters, and multi-tab sync.' + - name: 'Framework adapter idioms' + slug: 'framework-adapter-idioms' + description: 'Applying the same Query concepts through React hooks, Vue composables, Solid resources, Svelte runes, Angular signals, Lit controllers, and Preact hooks.' + - name: 'Operational quality' + slug: 'operational-quality' + description: 'Debugging with devtools, testing isolation, linting best practices, migration, codemods, and production readiness.' + +skills: + - name: 'Set up QueryClient and providers' + slug: 'setup-query-client-and-providers' + domain: 'bootstrapping-query-clients' + description: 'Install the right adapter, create stable QueryClient instances, and wire framework providers without leaking cache between renders or requests.' + type: 'lifecycle' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + - '@tanstack/svelte-query' + - '@tanstack/angular-query-experimental' + - '@tanstack/lit-query' + covers: + - 'QueryClient' + - 'QueryClientProvider' + - 'VueQueryPlugin' + - 'provideTanStackQuery' + - 'Lit QueryClientProvider custom element' + - 'useQueryClient and explicit clients' + tasks: + - 'Create a client in an SPA' + - 'Create per-request clients for SSR' + - 'Wire provider context for framework adapters' + failure_modes: + - mistake: 'New client on every render' + mechanism: 'The QueryClient owns the caches; recreating it discards cache and subscriptions.' + wrong_pattern: | + function App() { + const queryClient = new QueryClient() + return {children} + } + correct_pattern: | + function App() { + const [queryClient] = React.useState(() => new QueryClient()) + return {children} + } + source: 'docs/eslint/stable-query-client.md' + priority: 'CRITICAL' + status: 'active' + - mistake: 'Shared SSR cache between users' + mechanism: "A module-level client in SSR can pass one user's dehydrated data to another request." + wrong_pattern: | + const queryClient = new QueryClient() + export default function MyApp() { + return {children} + } + correct_pattern: | + export default function MyApp() { + const [queryClient] = React.useState(() => new QueryClient()) + return {children} + } + source: 'docs/framework/react/guides/ssr.md' + priority: 'CRITICAL' + status: 'active' + skills: + ['setup-query-client-and-providers', 'ssr-hydration-and-streaming'] + - mistake: 'Ambiguous Lit fallback client' + mechanism: 'Lit fallback lookup throws if multiple distinct QueryClientProvider instances are connected.' + wrong_pattern: | + const client = useQueryClient() + createQueryController(this, { queryKey: ['todos'], queryFn: fetchTodos }) + correct_pattern: | + createQueryController(this, { + queryKey: ['todos'], + queryFn: fetchTodos, + }, explicitClient) + source: 'docs/framework/lit/guides/reactive-controllers-vs-hooks.md' + priority: 'HIGH' + status: 'active' + + - name: 'Design query keys and reusable options' + slug: 'design-query-keys-and-options' + domain: 'reading-server-state' + description: 'Model query identity, dependencies, query functions, queryOptions, infiniteQueryOptions, mutationOptions, and skipToken in a type-safe reusable way.' + type: 'core' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + - '@tanstack/svelte-query' + - '@tanstack/angular-query-experimental' + - '@tanstack/lit-query' + covers: + - 'queryKey' + - 'queryFn' + - 'queryOptions' + - 'infiniteQueryOptions' + - 'mutationOptions' + - 'skipToken' + - 'global Register types' + tasks: + - 'Build a key factory' + - 'Extract reusable options' + - 'Use typed keys with QueryClient methods' + reference_candidates: + - topic: 'Reusable option helper signatures across adapters' + reason: 'Each adapter exposes equivalent helpers with framework-specific generic and reactivity surfaces.' + failure_modes: + - mistake: 'Missing variables in query key' + mechanism: 'Query keys are dependencies; variables used by queryFn must be part of the key to cache and refetch independently.' + wrong_pattern: | + useQuery({ + queryKey: ['todo'], + queryFn: () => api.getTodo(todoId), + }) + correct_pattern: | + useQuery({ + queryKey: ['todo', todoId], + queryFn: () => api.getTodo(todoId), + }) + source: 'docs/framework/react/guides/query-keys.md; docs/eslint/exhaustive-deps.md' + priority: 'CRITICAL' + status: 'active' + - mistake: 'Separated key and queryFn drift' + mechanism: 'Duplicating keys across hook calls and QueryClient methods allows the same key to point at different functions.' + wrong_pattern: | + useQuery({ queryKey: ['todo', id], queryFn: () => api.getTodo(id) }) + queryClient.invalidateQueries({ queryKey: ['todo', id] }) + correct_pattern: | + const todoOptions = (id) => queryOptions({ + queryKey: ['todo', id], + queryFn: () => api.getTodo(id), + }) + useQuery(todoOptions(id)) + queryClient.invalidateQueries({ queryKey: todoOptions(id).queryKey }) + source: 'docs/framework/react/guides/query-options.md; docs/eslint/prefer-query-options.md' + priority: 'HIGH' + status: 'active' + - mistake: 'skipToken inside suspense query' + mechanism: 'Suspense hooks require data and do not support conditional disabling through skipToken.' + wrong_pattern: | + useSuspenseQuery({ + queryKey: ['user', id], + queryFn: id ? () => getUser(id) : skipToken, + }) + correct_pattern: | + const query = useQuery({ + queryKey: ['user', id], + queryFn: id ? () => getUser(id) : skipToken, + }) + source: 'packages/react-query/src/useSuspenseQuery.ts; docs/framework/react/guides/suspense.md' + priority: 'HIGH' + status: 'active' + skills: + ['design-query-keys-and-options', 'use-suspense-and-error-boundaries'] + + - name: 'Fetch and observe queries' + slug: 'fetch-and-observe-queries' + domain: 'reading-server-state' + description: 'Use query APIs to read server state, interpret status and fetchStatus, handle errors, and keep query functions pure and cacheable.' + type: 'core' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + - '@tanstack/svelte-query' + - '@tanstack/angular-query-experimental' + - '@tanstack/lit-query' + covers: + - 'useQuery' + - 'createQuery' + - 'injectQuery' + - 'createQueryController' + - 'QueryObserver' + - 'status and fetchStatus' + - 'query function context' + tasks: + - 'Render loading, error, and success states' + - 'Use AbortSignal from QueryFunctionContext' + - 'Differentiate data status from fetch status' + failure_modes: + - mistake: 'Void query function' + mechanism: 'TanStack Query caches the resolved value; undefined data is a configuration error.' + wrong_pattern: | + useQuery({ + queryKey: ['todos'], + queryFn: async () => { + await api.todos.fetch() + }, + }) + correct_pattern: | + useQuery({ + queryKey: ['todos'], + queryFn: async () => { + return await api.todos.fetch() + }, + }) + source: 'docs/eslint/no-void-query-fn.md; docs/framework/react/reference/useQuery.md' + priority: 'CRITICAL' + status: 'active' + - mistake: 'Only checking isPending for offline state' + mechanism: 'A pending query can have fetchStatus paused when networkMode prevents execution.' + wrong_pattern: | + if (query.isPending) return + correct_pattern: | + if (query.isPending && query.fetchStatus === 'paused') return + if (query.isPending) return + source: 'docs/framework/react/guides/queries.md; docs/framework/react/guides/network-mode.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Using mutations for reads' + mechanism: 'Queries represent idempotent server-state reads; writes and side effects belong in mutations.' + wrong_pattern: | + useQuery({ + queryKey: ['createTodo'], + queryFn: () => api.createTodo(input), + }) + correct_pattern: | + useMutation({ + mutationFn: (input) => api.createTodo(input), + }) + source: 'docs/framework/react/guides/queries.md; docs/framework/react/guides/mutations.md' + priority: 'HIGH' + status: 'active' + + - name: 'Tune defaults, freshness, retries, and refetching' + slug: 'tune-defaults-freshness-retries-and-refetching' + domain: 'coordinating-query-execution' + description: 'Apply important defaults, staleTime, gcTime, retry, refetch triggers, polling, focusManager, onlineManager, and networkMode.' + type: 'core' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + - '@tanstack/svelte-query' + - '@tanstack/angular-query-experimental' + - '@tanstack/lit-query' + covers: + - 'staleTime' + - 'gcTime' + - 'retry and retryDelay' + - 'refetchOnWindowFocus' + - 'refetchOnReconnect' + - 'networkMode' + - 'focusManager' + - 'onlineManager' + tasks: + - 'Prevent excessive refetches' + - 'Set cache retention policy' + - 'Support offline-first or always-available queryFns' + failure_modes: + - mistake: 'Confusing gcTime with freshness' + mechanism: 'gcTime only applies after a query becomes unused; staleTime controls freshness and refetching.' + wrong_pattern: | + useQuery({ + queryKey: ['todos'], + queryFn: fetchTodos, + gcTime: 60_000, + }) + correct_pattern: | + useQuery({ + queryKey: ['todos'], + queryFn: fetchTodos, + staleTime: 60_000, + }) + source: 'docs/framework/react/guides/important-defaults.md; docs/framework/react/guides/migrating-to-v5.md' + priority: 'CRITICAL' + status: 'active' + - mistake: 'Static staleTime blocks invalidation' + mechanism: "staleTime: 'static' is stricter than Infinity and ignores manual invalidation refetches." + wrong_pattern: | + useQuery({ + queryKey: ['permissions'], + queryFn: fetchPermissions, + staleTime: 'static', + }) + queryClient.invalidateQueries({ queryKey: ['permissions'] }) + correct_pattern: | + useQuery({ + queryKey: ['permissions'], + queryFn: fetchPermissions, + staleTime: Infinity, + }) + queryClient.invalidateQueries({ queryKey: ['permissions'] }) + source: 'docs/framework/react/guides/important-defaults.md; docs/reference/QueryClient.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Tests hang on default retries' + mechanism: 'Failed queries retry three times with exponential backoff by default.' + wrong_pattern: | + renderHook(() => useQuery({ queryKey: ['x'], queryFn: failingFn }), { wrapper }) + correct_pattern: | + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }) + source: 'docs/framework/react/guides/testing.md; docs/framework/angular/guides/testing.md' + priority: 'HIGH' + status: 'active' + skills: + ['tune-defaults-freshness-retries-and-refetching', 'test-query-code'] + + - name: 'Coordinate dependent, parallel, disabled, and background queries' + slug: 'coordinate-dependent-parallel-disabled-and-background-queries' + domain: 'coordinating-query-execution' + description: 'Compose multiple queries, dependent queries, disabled queries, background indicators, polling, and scroll restoration.' + type: 'core' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + - '@tanstack/svelte-query' + - '@tanstack/angular-query-experimental' + - '@tanstack/lit-query' + covers: + - 'useQueries' + - 'enabled' + - 'skipToken' + - 'useIsFetching' + - 'refetchInterval' + - 'scroll restoration' + tasks: + - 'Fetch multiple independent resources' + - 'Fetch after another query succeeds' + - 'Show background fetching without replacing content' + failure_modes: + - mistake: 'Imperative disabled query as lazy fetch' + mechanism: 'Permanently disabling queries opts out of invalidation and background behavior.' + wrong_pattern: | + const query = useQuery({ + queryKey: ['todos'], + queryFn: fetchTodos, + enabled: false, + }) + return + correct_pattern: | + const query = useQuery({ + queryKey: ['todos', filters], + queryFn: () => fetchTodos(filters), + enabled: Boolean(filters), + }) + source: 'docs/framework/react/guides/disabling-queries.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Duplicate keys in useQueries' + mechanism: 'The same key can share data between entries and confuse placeholder or select behavior.' + wrong_pattern: | + useQueries({ + queries: ids.map(() => ({ queryKey: ['user'], queryFn: fetchUser })), + }) + correct_pattern: | + useQueries({ + queries: ids.map((id) => ({ queryKey: ['user', id], queryFn: () => fetchUser(id) })), + }) + source: 'docs/framework/react/reference/useQueries.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Showing full-page spinner on background refetch' + mechanism: 'isFetching includes background refetches while status describes whether data exists.' + wrong_pattern: | + if (query.isFetching) return + correct_pattern: | + if (query.isPending) return + return <>{query.isFetching ? : null} + source: 'docs/framework/react/guides/background-fetching-indicators.md; docs/framework/react/guides/queries.md' + priority: 'MEDIUM' + status: 'active' + + - name: 'Paginate and build infinite queries' + slug: 'paginate-and-build-infinite-queries' + domain: 'coordinating-query-execution' + description: 'Implement page-indexed queries, placeholderData, infinite queries, maxPages, bi-directional lists, and safe manual infinite cache updates.' + type: 'core' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + - '@tanstack/svelte-query' + - '@tanstack/angular-query-experimental' + - '@tanstack/lit-query' + covers: + - 'useInfiniteQuery' + - 'createInfiniteQuery' + - 'injectInfiniteQuery' + - 'createInfiniteQueryController' + - 'initialPageParam' + - 'getNextPageParam' + - 'maxPages' + - 'keepPreviousData' + tasks: + - 'Build numbered pagination' + - 'Build load-more and infinite scroll' + - 'Limit stored pages' + failure_modes: + - mistake: 'Missing initialPageParam' + mechanism: 'v5 infinite queries require an explicit initial page param.' + wrong_pattern: | + useInfiniteQuery({ + queryKey: ['projects'], + queryFn: fetchProjects, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }) + correct_pattern: | + useInfiniteQuery({ + queryKey: ['projects'], + queryFn: fetchProjects, + initialPageParam: 0, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }) + source: 'docs/framework/react/guides/infinite-queries.md; docs/framework/react/guides/migrating-to-v5.md' + priority: 'CRITICAL' + status: 'active' + - mistake: 'Overlapping infinite fetches' + mechanism: 'One cache entry is shared for all pages; fetchNextPage during an active fetch can overwrite background refresh data.' + wrong_pattern: | + fetchNextPage()} /> + correct_pattern: | + hasNextPage && !isFetching && fetchNextPage()} /> + source: 'docs/framework/react/guides/infinite-queries.md; docs/framework/lit/guides/infinite-queries.md' + priority: 'CRITICAL' + status: 'active' + - mistake: 'Breaking infinite data shape' + mechanism: 'Infinite query cache data must always keep pages and pageParams arrays.' + wrong_pattern: | + queryClient.setQueryData(['projects'], (data) => data.pages.slice(1)) + correct_pattern: | + queryClient.setQueryData(['projects'], (data) => ({ + pages: data.pages.slice(1), + pageParams: data.pageParams.slice(1), + })) + source: 'docs/framework/react/guides/infinite-queries.md' + priority: 'HIGH' + status: 'active' + + - name: 'Write mutations and invalidate related queries' + slug: 'write-mutations-and-invalidate-related-queries' + domain: 'writing-server-state' + description: 'Use mutations for server writes, compose side effects, invalidate related reads, update from mutation responses, and manage mutation state.' + type: 'core' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + - '@tanstack/svelte-query' + - '@tanstack/angular-query-experimental' + - '@tanstack/lit-query' + covers: + - 'useMutation' + - 'createMutation' + - 'injectMutation' + - 'createMutationController' + - 'invalidateQueries' + - 'useMutationState' + - 'mutationOptions' + tasks: + - 'Create, update, or delete server data' + - 'Invalidate after mutation success' + - 'Share pending mutation state across components' + failure_modes: + - mistake: 'Multiple mutate arguments' + mechanism: 'mutate accepts one variables value; use an object for multiple fields.' + wrong_pattern: | + mutation.mutate(title, body) + correct_pattern: | + mutation.mutate({ title, body }) + source: 'docs/framework/react/guides/mutations.md; TKDodo Mastering Mutations' + priority: 'HIGH' + status: 'active' + - mistake: 'Per-call callbacks expected after unmount' + mechanism: 'Callbacks passed to mutate run only if the observer is still mounted and only for the last consecutive call.' + wrong_pattern: | + todos.forEach((todo) => mutate(todo, { onSuccess: toastSuccess })) + correct_pattern: | + const mutation = useMutation({ + mutationFn: addTodo, + onSuccess: toastSuccess, + }) + todos.forEach((todo) => mutation.mutate(todo)) + source: 'docs/framework/react/guides/mutations.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Not awaiting invalidation when pending UI depends on it' + mechanism: 'Returning the invalidation promise keeps the mutation pending until refetch finishes.' + wrong_pattern: | + useMutation({ + mutationFn: addTodo, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['todos'] }) + }, + }) + correct_pattern: | + useMutation({ + mutationFn: addTodo, + onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), + }) + source: 'docs/framework/react/guides/optimistic-updates.md; TKDodo Automatic Query Invalidation after Mutations' + priority: 'HIGH' + status: 'active' + + - name: 'Implement optimistic updates and cache writes' + slug: 'implement-optimistic-updates-and-cache-writes' + domain: 'writing-server-state' + description: 'Choose UI-level optimistic rendering or direct cache writes, cancel stale reads, snapshot, rollback, and refetch safely.' + type: 'core' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + - '@tanstack/svelte-query' + - '@tanstack/angular-query-experimental' + - '@tanstack/lit-query' + covers: + - 'onMutate' + - 'onError' + - 'onSettled' + - 'cancelQueries' + - 'setQueryData' + - 'useMutationState' + tasks: + - 'Show pending items with variables' + - 'Write optimistic cache updates' + - 'Roll back failed mutations' + failure_modes: + - mistake: 'Optimistic write without cancelling refetch' + mechanism: 'An in-flight refetch can resolve after the optimistic write and clobber it.' + wrong_pattern: | + onMutate: (newTodo) => { + queryClient.setQueryData(['todos'], (old) => [...old, newTodo]) + } + correct_pattern: | + onMutate: async (newTodo, context) => { + await context.client.cancelQueries({ queryKey: ['todos'] }) + const previousTodos = context.client.getQueryData(['todos']) + context.client.setQueryData(['todos'], (old) => [...old, newTodo]) + return { previousTodos } + } + source: 'docs/framework/react/guides/optimistic-updates.md' + priority: 'CRITICAL' + status: 'active' + - mistake: 'Mutating cached data in place' + mechanism: 'Cache writes must be immutable to preserve observer notifications and structural sharing.' + wrong_pattern: | + queryClient.setQueryData(['todo', id], (old) => { + old.title = title + return old + }) + correct_pattern: | + queryClient.setQueryData(['todo', id], (old) => ({ + ...old, + title, + })) + source: 'docs/reference/QueryClient.md; docs/framework/react/guides/updates-from-mutation-responses.md' + priority: 'CRITICAL' + status: 'active' + skills: + [ + 'implement-optimistic-updates-and-cache-writes', + 'shape-data-and-render-efficiently', + ] + - mistake: 'Persisted persister loses optimistic cache write' + mechanism: 'experimental_createQueryPersister does not persist setQueryData writes until invalidation/refetch persists real data.' + wrong_pattern: | + context.client.setQueryData(['todos'], optimisticTodos) + // user refreshes before invalidation finishes + correct_pattern: | + context.client.setQueryData(['todos'], optimisticTodos) + return context.client.invalidateQueries({ queryKey: ['todos'] }) + source: 'docs/framework/react/plugins/createPersister.md; docs/framework/vue/plugins/createPersister.md' + priority: 'HIGH' + status: 'active' + skills: + [ + 'implement-optimistic-updates-and-cache-writes', + 'persist-offline-and-restore-caches', + ] + + - name: 'Cancel queries and consume AbortSignals' + slug: 'cancel-queries-and-consume-abort-signals' + domain: 'writing-server-state' + description: 'Use AbortSignal-aware query functions, manual cancellation, cancellation options, and understand state revert behavior.' + type: 'core' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + - '@tanstack/svelte-query' + - '@tanstack/angular-query-experimental' + - '@tanstack/lit-query' + covers: + - 'QueryFunctionContext.signal' + - 'cancelQueries' + - 'CancelledError' + - 'cancel options' + - 'axios and fetch cancellation' + tasks: + - 'Abort fetch requests on key changes' + - 'Cancel a slow query from UI' + - 'Protect optimistic updates from stale refetches' + failure_modes: + - mistake: 'Ignoring signal in long request' + mechanism: 'Without consuming signal, unused queries can still resolve and populate cache.' + wrong_pattern: | + useQuery({ + queryKey: ['todos'], + queryFn: () => fetch('/todos').then((r) => r.json()), + }) + correct_pattern: | + useQuery({ + queryKey: ['todos'], + queryFn: ({ signal }) => fetch('/todos', { signal }).then((r) => r.json()), + }) + source: 'docs/framework/react/guides/query-cancellation.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Assuming unmount always cancels' + mechanism: 'Default behavior keeps unresolved query promises running unless the signal is consumed.' + wrong_pattern: | + // rely on component unmount to abort the network request + correct_pattern: | + queryFn: async ({ signal }) => { + const response = await fetch(url, { signal }) + return response.json() + } + source: 'docs/framework/react/guides/query-cancellation.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Suspense cancellation expected' + mechanism: 'Cancellation does not work with Suspense hooks.' + wrong_pattern: | + const query = useSuspenseQuery({ queryKey, queryFn }) + await queryClient.cancelQueries({ queryKey }) + correct_pattern: | + const query = useQuery({ queryKey, queryFn }) + await queryClient.cancelQueries({ queryKey }) + source: 'docs/framework/react/guides/query-cancellation.md' + priority: 'MEDIUM' + status: 'active' + skills: + [ + 'cancel-queries-and-consume-abort-signals', + 'use-suspense-and-error-boundaries', + ] + + - name: 'Seed, placeholder, select, and transform data' + slug: 'seed-placeholder-select-and-transform-data' + domain: 'shaping-cache-and-render-behavior' + description: 'Use initialData, placeholderData, keepPreviousData, select, queryOptions, and cache seeding without corrupting timestamps or shapes.' + type: 'core' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + - '@tanstack/svelte-query' + - '@tanstack/angular-query-experimental' + - '@tanstack/lit-query' + covers: + - 'initialData' + - 'placeholderData' + - 'keepPreviousData' + - 'select' + - 'dataUpdatedAt' + - 'ensureQueryData' + tasks: + - 'Seed detail data from a list' + - 'Keep previous page visible' + - 'Transform returned data for a component' + failure_modes: + - mistake: 'Expecting initialData to overwrite fresher cache' + mechanism: 'initialData never overwrites existing cache data, even if the incoming value is fresher.' + wrong_pattern: | + useQuery({ queryKey: ['posts'], queryFn: getPosts, initialData: props.posts }) + correct_pattern: | + await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts }) + return + source: 'docs/framework/react/guides/ssr.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Using v4 keepPreviousData option' + mechanism: 'v5 replaced keepPreviousData option with placeholderData identity.' + wrong_pattern: | + useQuery({ + queryKey, + queryFn, + keepPreviousData: true, + }) + correct_pattern: | + useQuery({ + queryKey, + queryFn, + placeholderData: keepPreviousData, + }) + source: 'docs/framework/react/guides/migrating-to-v5.md; docs/framework/react/guides/paginated-queries.md' + priority: 'CRITICAL' + status: 'fixed-but-legacy-risk' + - mistake: 'Selector throwing breaks observer' + mechanism: 'select should transform successful data; errors belong in queryFn so Query state can represent them.' + wrong_pattern: | + useQuery({ + queryKey: ['todos'], + queryFn: fetchTodos, + select: (data) => { + if (!data.length) throw new Error('empty') + return data + }, + }) + correct_pattern: | + useQuery({ + queryKey: ['todos'], + queryFn: async () => { + const data = await fetchTodos() + if (!data.length) throw new Error('empty') + return data + }, + }) + source: 'packages/query-core/src/queryObserver.ts; docs/framework/react/guides/render-optimizations.md' + priority: 'MEDIUM' + status: 'active' + + - name: 'Shape data and render efficiently' + slug: 'shape-data-and-render-efficiently' + domain: 'shaping-cache-and-render-behavior' + description: 'Use structural sharing, tracked properties, select functions, memoized combine functions, and adapter-specific immutable result rules.' + type: 'framework' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + covers: + - 'structuralSharing' + - 'notifyOnChangeProps' + - 'select' + - 'combine' + - 'tracked properties' + - 'no-rest-destructuring' + - 'no-unstable-deps' + tasks: + - 'Avoid unnecessary rerenders' + - 'Select small slices from large responses' + - 'Keep immutable query results safe in forms' + failure_modes: + - mistake: 'Rest destructuring disables tracked props' + mechanism: 'Object rest touches every property and opts out of property-level tracking.' + wrong_pattern: | + const { data, ...queryInfo } = useQuery(options) + correct_pattern: | + const { data, isPending, isError, error } = useQuery(options) + source: 'docs/eslint/no-rest-destructuring.md; docs/framework/react/guides/render-optimizations.md' + priority: 'MEDIUM' + status: 'active' + - mistake: 'Query result in hook deps' + mechanism: 'Query result objects are not referentially stable; destructure stable members.' + wrong_pattern: | + const query = useQuery(options) + useEffect(() => sync(query.data), [query]) + correct_pattern: | + const { data } = useQuery(options) + useEffect(() => sync(data), [data]) + source: 'docs/eslint/no-unstable-deps.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Vue v-model mutates query result' + mechanism: 'Vue Query results are immutable; two-way binding needs a mutable copy.' + wrong_pattern: | + + correct_pattern: | + const editableTodo = ref({ ...todo.value }) + + source: 'docs/framework/vue/reactivity.md' + priority: 'HIGH' + status: 'active' + + - name: 'Prefetch and remove request waterfalls' + slug: 'prefetch-and-remove-request-waterfalls' + domain: 'rendering-across-environments' + description: 'Prefetch data in event handlers, component lifecycle, TanStack Router and Start loaders, other routers, and query functions to flatten waterfalls before render.' + type: 'lifecycle' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + - '@tanstack/svelte-query' + covers: + - 'prefetchQuery' + - 'prefetchInfiniteQuery' + - 'ensureQueryData' + - 'usePrefetchQuery' + - 'TanStack Router loaders' + - 'TanStack Start routes' + - 'router loaders' + - 'request waterfalls' + tasks: + - 'Prefetch on hover or focus' + - 'Prefetch child data from parent routes' + - 'Pair Query with TanStack Router loaders and TanStack Start routes' + - 'Pair Query with React Router loaders when the app uses React Router' + compositions: + - library: 'TanStack Start' + skill: 'Compose TanStack Query with Start routes and SSR' + - library: 'TanStack Router' + skill: 'Compose TanStack Query with TanStack Router loaders and SSR Query integration' + - library: 'React Router' + skill: 'Compose TanStack Query with React Router loaders' + failure_modes: + - mistake: 'Expecting prefetchQuery to return data' + mechanism: 'prefetchQuery returns void and swallows errors; use fetchQuery when data or thrown errors are needed.' + wrong_pattern: | + const data = await queryClient.prefetchQuery(todoOptions(id)) + correct_pattern: | + const data = await queryClient.fetchQuery(todoOptions(id)) + source: 'docs/framework/react/guides/prefetching.md; docs/reference/QueryClient.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Prefetch staleTime only set on prefetch' + mechanism: 'staleTime passed to prefetch applies only to that prefetch call; useQuery still needs its own freshness policy.' + wrong_pattern: | + await queryClient.prefetchQuery({ queryKey, queryFn, staleTime: 60_000 }) + useQuery({ queryKey, queryFn }) + correct_pattern: | + const options = queryOptions({ queryKey, queryFn, staleTime: 60_000 }) + await queryClient.prefetchQuery(options) + useQuery(options) + source: 'docs/framework/react/guides/prefetching.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Suspense prefetch after suspension' + mechanism: 'Effects do not run until after a suspenseful query resolves, so they cannot flatten that waterfall.' + wrong_pattern: | + const article = useSuspenseQuery(articleOptions(id)) + useEffect(() => { + queryClient.prefetchQuery(commentsOptions(id)) + }, [id]) + correct_pattern: | + usePrefetchQuery(commentsOptions(id)) + return
+ source: 'docs/framework/react/guides/prefetching.md' + priority: 'HIGH' + status: 'active' + + - name: 'Compose TanStack Query with TanStack Router and Start' + slug: 'compose-query-with-tanstack-router-and-start' + domain: 'rendering-across-environments' + description: 'Wire QueryClient through TanStack Router context, Start routes, route loaders, and the Router SSR Query integration so route preloading, SSR hydration, streaming, and redirects are handled in the TanStack stack before reaching for Next.js-specific recipes.' + type: 'composition' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + ecosystem_targets: + - '@tanstack/react-router' + - '@tanstack/react-router-ssr-query' + - '@tanstack/react-start' + - '@tanstack/solid-start' + covers: + - 'createRouter context' + - 'createFileRoute loaders' + - 'ensureQueryData in loaders' + - 'setupRouterSsrQueryIntegration' + - 'Start SSR and streaming' + - 'redirect propagation' + - 'router.invalidate after loader errors' + tasks: + - 'Provide a per-request QueryClient through router context' + - 'Preload critical route data with loaders and read it with Query hooks' + - 'Use Router SSR Query integration for dehydration, hydration, streaming, and redirects' + - 'Treat TanStack Start as the default TanStack SSR path before manual Router SSR or Next.js' + compositions: + - library: 'TanStack Start' + skill: 'Use Query in Start routes, server functions, SSR, and streaming' + - library: 'TanStack Router' + skill: 'Use Query with route loaders and @tanstack/react-router-ssr-query' + failure_modes: + - mistake: 'Component-only Query creates a route waterfall' + mechanism: 'The router knows the next route before render; critical route data should be ensured in the loader and then read by the component.' + wrong_pattern: | + export const Route = createFileRoute('/posts')({ + component: PostsPage, + }) + function PostsPage() { + const { data } = useSuspenseQuery(postsQuery) + return + } + correct_pattern: | + export const Route = createFileRoute('/posts')({ + loader: ({ context }) => context.queryClient.ensureQueryData(postsQuery), + component: PostsPage, + }) + function PostsPage() { + const { data } = useSuspenseQuery(postsQuery) + return + } + source: 'https://tanstack.com/router/latest/docs/integrations/query' + priority: 'CRITICAL' + status: 'active' + - mistake: 'Hand-rolled Router hydration while integration exists' + mechanism: 'The Router SSR Query integration automates QueryClient dehydration, hydration, streamed query results, redirect handling, and optional provider wrapping.' + wrong_pattern: | + const router = createRouter({ routeTree }) + return ( + + + + + + ) + correct_pattern: | + const router = createRouter({ routeTree, context: { queryClient } }) + setupRouterSsrQueryIntegration({ router, queryClient }) + source: 'https://tanstack.com/router/latest/docs/integrations/query' + priority: 'CRITICAL' + status: 'active' + - mistake: 'Next.js mental model copied into Start' + mechanism: 'Start is powered by TanStack Router; generated guidance should use file routes, loaders, server functions, and router SSR instead of Next app/pages APIs.' + wrong_pattern: | + // app/posts/page.tsx + export default async function Page() { + const posts = await getPosts() + return + } + correct_pattern: | + export const Route = createFileRoute('/posts')({ + loader: ({ context }) => context.queryClient.ensureQueryData(postsQuery), + component: PostsPage, + }) + source: 'https://tanstack.com/start/latest/docs/framework/react/overview; https://tanstack.com/router/latest/docs/how-to/setup-ssr' + priority: 'HIGH' + status: 'active' + + - name: 'SSR, hydration, and streaming' + slug: 'ssr-hydration-and-streaming' + domain: 'rendering-across-environments' + description: 'Use prefetch, dehydrate, hydrate, HydrationBoundary, TanStack Start, Router SSR Query integration, Server Components, SvelteKit, Nuxt, SolidStart, Lit SSR, and streamed hydration safely.' + type: 'lifecycle' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/react-query-next-experimental' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + - '@tanstack/svelte-query' + - '@tanstack/lit-query' + ecosystem_targets: + - 'TanStack Start' + - '@tanstack/react-start' + - '@tanstack/solid-start' + - 'TanStack Router' + - '@tanstack/react-router' + - '@tanstack/react-router-ssr-query' + covers: + - 'dehydrate' + - 'hydrate' + - 'HydrationBoundary' + - 'setupRouterSsrQueryIntegration' + - 'TanStack Start SSR' + - 'Router streaming SSR' + - 'ReactQueryStreamedHydration' + - 'environmentManager' + - 'SvelteKit browser' + - 'Nuxt onServerPrefetch' + tasks: + - 'Treat TanStack Start as the default TanStack SSR path' + - 'Use Router SSR Query integration before hand-written Router hydration' + - 'Build SSR pages without double-fetching' + - 'Support Next.js app router and Server Components when the app is Next.js' + - 'Hydrate data in Vue, Svelte, Solid, or Lit' + subsystems: + - name: 'TanStack Start' + package: '@tanstack/react-start / @tanstack/solid-start' + config_surface: 'Start routes, server functions, full-document SSR, streaming' + - name: 'TanStack Router SSR Query' + package: '@tanstack/react-router-ssr-query' + config_surface: 'setupRouterSsrQueryIntegration, router context QueryClient, loader prefetch' + - name: 'Next.js pages router' + package: '@tanstack/react-query' + config_surface: 'getStaticProps or getServerSideProps plus HydrationBoundary' + - name: 'Next.js app router and streaming' + package: '@tanstack/react-query-next-experimental' + config_surface: 'client provider, environmentManager, ReactQueryStreamedHydration' + - name: 'Nuxt' + package: '@tanstack/vue-query' + config_surface: 'VueQueryPlugin, dehydrate/hydrate, onServerPrefetch' + - name: 'SvelteKit' + package: '@tanstack/svelte-query' + config_surface: 'browser-gated default enabled, load functions, QueryClientProvider' + failure_modes: + - mistake: 'RSC renders fetched data twice' + mechanism: 'React Query cannot revalidate Server Component-rendered data, so client refetch can desynchronize server-rendered derived output.' + wrong_pattern: | + const posts = await queryClient.fetchQuery(postsOptions) + return <>
{posts.length}
+ correct_pattern: | + await queryClient.prefetchQuery(postsOptions) + return + source: 'docs/framework/react/guides/advanced-ssr.md' + priority: 'CRITICAL' + status: 'active' + - mistake: 'Suspense query not prefetched on server' + mechanism: 'Forgotten prefetch can fetch on server without hydrating, then fetch again on client and cause mismatch.' + wrong_pattern: | + function Posts() { + const { data } = useSuspenseQuery(postsOptions) + return + } + correct_pattern: | + await queryClient.prefetchQuery(postsOptions) + return + source: 'docs/framework/react/guides/ssr.md' + priority: 'CRITICAL' + status: 'active' + skills: + ['ssr-hydration-and-streaming', 'use-suspense-and-error-boundaries'] + - mistake: 'SvelteKit query runs after SSR response' + mechanism: 'SvelteKit SSR needs queries disabled on the server unless explicitly prefetched.' + wrong_pattern: | + const queryClient = new QueryClient() + correct_pattern: | + const queryClient = new QueryClient({ + defaultOptions: { queries: { enabled: browser } }, + }) + source: 'docs/framework/svelte/ssr.md' + priority: 'HIGH' + status: 'active' + + - name: 'Use Suspense and error boundaries' + slug: 'use-suspense-and-error-boundaries' + domain: 'rendering-across-environments' + description: 'Use suspense hooks, QueryErrorResetBoundary, throwOnError, streamed hydration, and React.use promise support with the right constraints.' + type: 'framework' + packages: + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/react-query-next-experimental' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + covers: + - 'useSuspenseQuery' + - 'useSuspenseQueries' + - 'useSuspenseInfiniteQuery' + - 'QueryErrorResetBoundary' + - 'throwOnError' + - 'experimental_prefetchInRender' + - 'React.use(query.promise)' + tasks: + - 'Render data with Suspense' + - 'Reset errors after retry' + - 'Use streamed hydration where the router or framework supports it' + failure_modes: + - mistake: 'Conditionally disabling suspense query' + mechanism: 'Suspense queries guarantee data and cannot be disabled like normal queries.' + wrong_pattern: | + useSuspenseQuery({ queryKey: ['user', id], queryFn: getUser, enabled: Boolean(id) }) + correct_pattern: | + const query = useQuery({ queryKey: ['user', id], queryFn: getUser, enabled: Boolean(id) }) + source: 'docs/framework/react/guides/suspense.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Missing QueryErrorResetBoundary' + mechanism: 'Error boundaries need reset coordination so failed queries can retry after boundary reset.' + wrong_pattern: | + + + + correct_pattern: | + + {({ reset }) => } + + source: 'docs/framework/react/guides/suspense.md' + priority: 'HIGH' + status: 'active' + - mistake: 'query.promise without feature flag' + mechanism: 'React.use(query.promise) requires experimental_prefetchInRender on the QueryClient.' + wrong_pattern: | + const query = useQuery(options) + const data = React.use(query.promise) + correct_pattern: | + const queryClient = new QueryClient({ + defaultOptions: { queries: { experimental_prefetchInRender: true } }, + }) + source: 'docs/framework/react/guides/suspense.md; packages/query-core/src/queryObserver.ts' + priority: 'MEDIUM' + status: 'active' + + - name: 'Persist offline and restore caches' + slug: 'persist-offline-and-restore-caches' + domain: 'persisting-and-synchronizing-caches' + description: 'Persist queries and mutations to storage, avoid restore races, resume paused mutations, tune gcTime/maxAge, and build custom persisters.' + type: 'composition' + packages: + - '@tanstack/query-persist-client-core' + - '@tanstack/query-async-storage-persister' + - '@tanstack/query-sync-storage-persister' + - '@tanstack/react-query-persist-client' + - '@tanstack/preact-query-persist-client' + - '@tanstack/solid-query-persist-client' + - '@tanstack/svelte-query-persist-client' + - '@tanstack/angular-query-persist-client' + covers: + - 'persistQueryClient' + - 'PersistQueryClientProvider' + - 'createAsyncStoragePersister' + - 'createSyncStoragePersister' + - 'experimental_createQueryPersister' + - 'resumePausedMutations' + - 'useIsRestoring' + tasks: + - 'Persist cache to storage' + - 'Restore without query races' + - 'Resume offline mutations' + subsystems: + - name: 'Async storage persister' + package: '@tanstack/query-async-storage-persister' + config_surface: 'storage, serialize, deserialize, throttleTime, retry' + - name: 'Sync storage persister' + package: '@tanstack/query-sync-storage-persister' + config_surface: 'deprecated sync storage persister options' + - name: 'Fine-grained query persister' + package: '@tanstack/query-persist-client-core' + config_surface: 'experimental_createQueryPersister storage and filter utilities' + failure_modes: + - mistake: 'gcTime shorter than maxAge' + mechanism: 'Hydrated cache can be garbage collected before persisted maxAge expires.' + wrong_pattern: | + const queryClient = new QueryClient() + persistQueryClient({ queryClient, persister, maxAge: DAY }) + correct_pattern: | + const queryClient = new QueryClient({ + defaultOptions: { queries: { gcTime: DAY } }, + }) + persistQueryClient({ queryClient, persister, maxAge: DAY }) + source: 'docs/framework/react/plugins/persistQueryClient.md' + priority: 'CRITICAL' + status: 'active' + - mistake: 'Rendering before async restore' + mechanism: 'Queries can mount and fetch while restore is still in progress.' + wrong_pattern: | + persistQueryClient({ queryClient, persister }) + root.render() + correct_pattern: | + root.render( + + + , + ) + source: 'docs/framework/react/plugins/persistQueryClient.md' + priority: 'CRITICAL' + status: 'active' + - mistake: 'No default mutationFn for persisted mutation' + mechanism: 'Paused persisted mutations need mutation defaults to resume after reload because functions cannot be serialized.' + wrong_pattern: | + useMutation({ mutationKey: ['addTodo'], mutationFn: addTodo }) + correct_pattern: | + queryClient.setMutationDefaults(['addTodo'], { mutationFn: addTodo }) + useMutation({ mutationKey: ['addTodo'] }) + source: 'docs/framework/react/guides/mutations.md; docs/framework/react/plugins/persistQueryClient.md' + priority: 'HIGH' + status: 'active' + + - name: 'Broadcast, realtime, and multi-tab synchronization' + slug: 'broadcast-realtime-and-multi-tab-synchronization' + domain: 'persisting-and-synchronizing-caches' + description: 'Use experimental broadcastQueryClient, WebSocket-driven invalidation or cache updates, and multi-window sync without over-normalizing.' + type: 'composition' + packages: + - '@tanstack/query-broadcast-client-experimental' + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + covers: + - 'broadcastQueryClient' + - 'BroadcastChannel' + - 'query invalidation from events' + - 'setQueryData from event payloads' + tasks: + - 'Sync cache changes across tabs' + - 'Invalidate queries from WebSocket events' + - 'Choose invalidation instead of normalized writes' + compositions: + - library: 'WebSocket clients' + skill: 'Invalidate or update Query cache from realtime events' + - library: 'BroadcastChannel' + skill: 'Broadcast cache between browser tabs' + failure_modes: + - mistake: 'Using experimental broadcast without version lock' + mechanism: 'The package is experimental and may break in minor or patch releases.' + wrong_pattern: | + pnpm add @tanstack/query-broadcast-client-experimental + correct_pattern: | + pnpm add @tanstack/query-broadcast-client-experimental@5.101.0 + source: 'docs/framework/react/plugins/broadcastQueryClient.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Manual normalized cache everywhere' + mechanism: 'TanStack Query prefers targeted invalidation and atomic cache writes over schema-level normalized cache maintenance.' + wrong_pattern: | + event.entities.todos.forEach((todo) => { + queryClient.setQueryData(['todos'], normalize(todo)) + }) + correct_pattern: | + queryClient.invalidateQueries({ queryKey: ['todos'] }) + source: 'docs/framework/react/guides/query-invalidation.md; TKDodo Using WebSockets with React Query' + priority: 'MEDIUM' + status: 'active' + - mistake: 'Broadcast used on server' + mechanism: 'BroadcastChannel-based synchronization is browser-oriented and should be gated from SSR environments.' + wrong_pattern: | + broadcastQueryClient({ queryClient, broadcastChannel: 'app' }) + correct_pattern: | + if (typeof window !== 'undefined') { + broadcastQueryClient({ queryClient, broadcastChannel: 'app' }) + } + source: 'docs/framework/react/plugins/broadcastQueryClient.md; package peer behavior' + priority: 'MEDIUM' + status: 'active' + + - name: 'Use framework adapter reactivity' + slug: 'use-framework-adapter-reactivity' + domain: 'framework-adapter-idioms' + description: 'Translate core Query patterns to adapter-specific reactivity: React hooks, Vue refs/getters, Solid resources, Svelte runes, Angular signals, Lit controllers, and Preact hooks.' + type: 'framework' + packages: + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + - '@tanstack/svelte-query' + - '@tanstack/angular-query-experimental' + - '@tanstack/lit-query' + covers: + - 'React hooks' + - 'Vue tracked query keys' + - 'Solid reactive options' + - 'Svelte thunked options' + - 'Angular injectQuery signals' + - 'Lit host-bound controllers' + subsystems: + - name: 'React and Preact hooks' + package: '@tanstack/react-query' + config_surface: 'object options, hooks, QueryClientProvider' + - name: 'Vue composables' + package: '@tanstack/vue-query' + config_surface: 'refs, reactive getters, MaybeRefOrGetter, custom context key' + - name: 'Svelte runes' + package: '@tanstack/svelte-query' + config_surface: 'options thunk and direct property access' + - name: 'Angular signals' + package: '@tanstack/angular-query-experimental' + config_surface: 'injectQuery thunk, provideTanStackQuery, HttpClient Observable conversion' + - name: 'Lit controllers' + package: '@tanstack/lit-query' + config_surface: 'ReactiveControllerHost, callable accessors, provider custom element' + tasks: + - 'Port a React Query example to another framework' + - 'Preserve reactivity in extracted composables' + - 'Choose adapter-specific provider and result access patterns' + failure_modes: + - mistake: 'Vue ref unwrapped before key' + mechanism: 'Unwrapping a ref before passing it to queryKey loses reactivity.' + wrong_pattern: | + const { data } = useUserProjects(userId.value) + correct_pattern: | + const { data } = useUserProjects(userId) + source: 'docs/framework/vue/reactivity.md' + priority: 'CRITICAL' + status: 'active' + - mistake: 'Svelte v5 store syntax in v6 adapter' + mechanism: 'The v6 Svelte adapter uses runes and direct result properties, not stores.' + wrong_pattern: | + const query = createQuery({ queryKey: ['todos'], queryFn: fetchTodos }) + {#if $query.isSuccess}{$query.data}{/if} + correct_pattern: | + const query = createQuery(() => ({ queryKey: ['todos'], queryFn: fetchTodos })) + {#if query.isSuccess}{query.data}{/if} + source: 'docs/framework/svelte/migrate-from-v5-to-v6.md' + priority: 'CRITICAL' + status: 'fixed-but-legacy-risk' + - mistake: 'Angular Observable returned directly' + mechanism: 'Query functions cache promises or values; Angular HttpClient Observables need conversion.' + wrong_pattern: | + queryFn: () => this.http.get('/todos') + correct_pattern: | + queryFn: () => lastValueFrom(this.http.get('/todos')) + source: 'docs/framework/angular/angular-httpclient-and-other-data-fetching-clients.md; docs/framework/angular/overview.md' + priority: 'HIGH' + status: 'active' + + - name: 'Debug with devtools' + slug: 'debug-with-devtools' + domain: 'operational-quality' + description: 'Install floating, embedded, panel, and production-lazy devtools for each adapter and inspect queries, mutations, cache state, and mocked offline behavior.' + type: 'framework' + packages: + - '@tanstack/query-devtools' + - '@tanstack/react-query-devtools' + - '@tanstack/preact-query-devtools' + - '@tanstack/vue-query-devtools' + - '@tanstack/solid-query-devtools' + - '@tanstack/svelte-query-devtools' + - '@tanstack/angular-query-experimental' + covers: + - 'ReactQueryDevtools' + - 'VueQueryDevtools' + - 'SolidQueryDevtools' + - 'SvelteQueryDevtools' + - 'Angular withDevtools' + - 'embedded panels' + - 'mock offline' + tasks: + - 'Install devtools in development' + - 'Lazy-load devtools in production' + - 'Inspect paused, stale, invalidated, and fetching queries' + failure_modes: + - mistake: 'Production devtools imported eagerly' + mechanism: 'Production builds should use production subpaths or lazy-loading APIs when exposing devtools.' + wrong_pattern: | + import { ReactQueryDevtools } from '@tanstack/react-query-devtools' + correct_pattern: | + import { ReactQueryDevtools } from '@tanstack/react-query-devtools/production' + source: 'docs/framework/react/devtools.md; docs/framework/angular/devtools.md' + priority: 'MEDIUM' + status: 'active' + - mistake: 'Mock offline changes actual network' + mechanism: 'Devtools mock offline sets onlineManager state and does not alter browser network.' + wrong_pattern: | + // expect browser network calls to fail because devtools mock offline is enabled + correct_pattern: | + // use browser devtools to simulate network, or onlineManager.setOnline(false) for Query behavior + source: 'docs/framework/react/guides/network-mode.md' + priority: 'MEDIUM' + status: 'active' + - mistake: 'Devtools before provider' + mechanism: 'Devtools need a QueryClient from provider context or explicit wiring.' + wrong_pattern: | + + + correct_pattern: | + + + + + source: 'docs/framework/react/devtools.md' + priority: 'MEDIUM' + status: 'active' + + - name: 'Test query code' + slug: 'test-query-code' + domain: 'operational-quality' + description: 'Build isolated QueryClient test wrappers, disable retries, clear caches, mock network clients, and assert async query and mutation behavior.' + type: 'lifecycle' + packages: + - '@tanstack/query-test-utils' + - '@tanstack/query-core' + - '@tanstack/react-query' + - '@tanstack/preact-query' + - '@tanstack/vue-query' + - '@tanstack/solid-query' + - '@tanstack/svelte-query' + - '@tanstack/angular-query-experimental' + covers: + - 'test QueryClient' + - 'retry false' + - 'queryClient.clear' + - 'waitFor' + - 'TestBed' + - 'network stubs' + tasks: + - 'Test a query hook or composable' + - 'Test mutations and optimistic updates' + - 'Avoid cache bleed between tests' + failure_modes: + - mistake: 'Reusing test client across tests' + mechanism: 'Query cache data can leak into later tests.' + wrong_pattern: | + const queryClient = new QueryClient() + const wrapper = ({ children }) => {children} + correct_pattern: | + function createWrapper() { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + return ({ children }) => {children} + } + source: 'docs/framework/react/guides/testing.md; docs/framework/angular/guides/testing.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Retry backoff hides assertion timing' + mechanism: 'Default retries delay failures, making tests slow or flaky.' + wrong_pattern: | + const client = new QueryClient() + correct_pattern: | + const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }) + source: 'docs/framework/react/guides/testing.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Asserting before async success' + mechanism: 'Queries update asynchronously; tests should wait for success state or rendered output.' + wrong_pattern: | + const { result } = renderHook(() => useQuery(options), { wrapper }) + expect(result.current.data).toEqual(data) + correct_pattern: | + const { result } = renderHook(() => useQuery(options), { wrapper }) + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + expect(result.current.data).toEqual(data) + source: 'docs/framework/react/guides/testing.md' + priority: 'HIGH' + status: 'active' + + - name: 'Migrate major versions and codemods' + slug: 'migrate-major-versions-and-codemods' + domain: 'operational-quality' + description: 'Migrate v3 to v4, v4 to v5, Svelte v5 adapter to v6, Vue v5 changes, removed overloads, renamed options, and codemods.' + type: 'lifecycle' + packages: + - '@tanstack/query-codemods' + - '@tanstack/react-query' + - '@tanstack/vue-query' + - '@tanstack/svelte-query' + - '@tanstack/query-core' + - '@tanstack/eslint-plugin-query' + covers: + - 'v5 object syntax' + - 'cacheTime to gcTime' + - 'keepPreviousData to placeholderData' + - 'query callbacks removed' + - 'Svelte runes migration' + - 'Vue useQueries ref return' + tasks: + - 'Run v5 remove-overloads codemod' + - 'Replace removed query callbacks' + - 'Migrate Svelte Query stores to runes' + failure_modes: + - mistake: 'v4 overload syntax in v5' + mechanism: 'v5 supports one object signature for hooks and QueryClient methods.' + wrong_pattern: | + useQuery(['todos'], fetchTodos, { staleTime: 1000 }) + correct_pattern: | + useQuery({ queryKey: ['todos'], queryFn: fetchTodos, staleTime: 1000 }) + source: 'docs/framework/react/guides/migrating-to-v5.md' + priority: 'CRITICAL' + status: 'fixed-but-legacy-risk' + - mistake: 'Query callbacks on useQuery' + mechanism: 'onSuccess, onError, and onSettled were removed from queries in v5.' + wrong_pattern: | + useQuery({ + queryKey: ['todos'], + queryFn: fetchTodos, + onSuccess: syncTodos, + }) + correct_pattern: | + const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) + useEffect(() => { + if (query.data) syncTodos(query.data) + }, [query.data]) + source: 'docs/framework/react/guides/migrating-to-v5.md' + priority: 'CRITICAL' + status: 'fixed-but-legacy-risk' + - mistake: 'cacheTime option in v5' + mechanism: 'cacheTime was renamed to gcTime to clarify it only runs after a query is unused.' + wrong_pattern: | + new QueryClient({ + defaultOptions: { queries: { cacheTime: 10 * 60 * 1000 } }, + }) + correct_pattern: | + new QueryClient({ + defaultOptions: { queries: { gcTime: 10 * 60 * 1000 } }, + }) + source: 'docs/framework/react/guides/migrating-to-v5.md' + priority: 'HIGH' + status: 'fixed-but-legacy-risk' + + - name: 'Enforce Query best practices with ESLint' + slug: 'enforce-query-best-practices-with-eslint' + domain: 'operational-quality' + description: 'Install and tune @tanstack/eslint-plugin-query recommended and strict rules to catch query key, client stability, property order, void queryFn, and option reuse mistakes.' + type: 'composition' + packages: + - '@tanstack/eslint-plugin-query' + - '@tanstack/react-query' + covers: + - 'flat/recommended' + - 'flat/recommended-strict' + - 'exhaustive-deps' + - 'stable-query-client' + - 'no-rest-destructuring' + - 'no-unstable-deps' + - 'prefer-query-options' + - 'property order rules' + tasks: + - 'Add plugin to flat config' + - 'Enable strict rules' + - 'Fix query key and option issues' + failure_modes: + - mistake: 'Strict queryOptions rule disabled accidentally' + mechanism: 'prefer-query-options is recommended strict, not baseline recommended.' + wrong_pattern: | + export default [ + ...pluginQuery.configs['flat/recommended'], + ] + correct_pattern: | + export default [ + ...pluginQuery.configs['flat/recommended-strict'], + ] + source: 'docs/eslint/eslint-plugin-query.md; docs/eslint/prefer-query-options.md' + priority: 'MEDIUM' + status: 'active' + - mistake: 'Infinite option property order breaks inference' + mechanism: 'Some inference-sensitive options need stable order for type inference.' + wrong_pattern: | + useInfiniteQuery({ + queryFn, + queryKey, + getNextPageParam, + initialPageParam, + }) + correct_pattern: | + useInfiniteQuery({ + queryKey, + queryFn, + initialPageParam, + getNextPageParam, + }) + source: 'docs/eslint/infinite-query-property-order.md' + priority: 'MEDIUM' + status: 'active' + - mistake: 'Mutation option property order breaks inference' + mechanism: 'mutationFn and mutationKey order is linted because inference depends on the option object shape.' + wrong_pattern: | + useMutation({ + onSuccess, + mutationFn, + }) + correct_pattern: | + useMutation({ + mutationFn, + onSuccess, + }) + source: 'docs/eslint/mutation-property-order.md' + priority: 'MEDIUM' + status: 'active' + + - name: 'Build Query abstractions' + slug: 'build-query-abstractions' + domain: 'reading-server-state' + description: 'Create queryOptions factories, feature-local key modules, and custom hooks that preserve inference and work across hooks, loaders, prefetches, and QueryClient calls.' + type: 'core' + packages: + - '@tanstack/react-query' + - '@tanstack/query-core' + covers: + - 'queryOptions factories' + - 'custom hooks on top of options' + - 'usage-site option composition' + - 'TypeScript select inference' + tasks: + - 'Extract a queryOptions factory' + - 'Share options between loader, hook, and prefetch' + - 'Avoid wide UseQueryOptions wrappers' + failure_modes: + - mistake: 'Custom hook is the only abstraction' + mechanism: 'Hooks cannot run in route loaders, server prefetches, or event handlers.' + wrong_pattern: | + export function useInvoice(id) { + return useQuery({ queryKey: ['invoice', id], queryFn: () => fetchInvoice(id) }) + } + correct_pattern: | + export function invoiceOptions(id) { + return queryOptions({ queryKey: ['invoice', id], queryFn: () => fetchInvoice(id) }) + } + export function useInvoice(id) { + return useQuery(invoiceOptions(id)) + } + source: 'https://tkdodo.eu/blog/creating-query-abstractions' + priority: 'HIGH' + status: 'active' + - mistake: 'Wide UseQueryOptions wrapper breaks inference' + mechanism: 'Generic options wrappers often lose select inference and key typing.' + wrong_pattern: | + function useInvoice(id, options?: Partial>) { + return useQuery({ queryKey: ['invoice', id], queryFn: () => fetchInvoice(id), ...options }) + } + correct_pattern: | + useQuery({ + ...invoiceOptions(id), + select: (invoice) => invoice.createdAt, + }) + source: 'https://tkdodo.eu/blog/creating-query-abstractions' + priority: 'HIGH' + status: 'active' + - mistake: 'Wrapper hides Query result state' + mechanism: 'Returning only data hides status, error, refetch, and background-fetch state from callers.' + wrong_pattern: | + export function useInvoice(id) { + return useQuery(invoiceOptions(id)).data + } + correct_pattern: | + export function useInvoice(id) { + return useQuery(invoiceOptions(id)) + } + source: 'docs/framework/react/guides/queries.md' + priority: 'MEDIUM' + status: 'active' + + - name: 'Understand Query internals and observers' + slug: 'understand-query-internals-and-observers' + domain: 'reading-server-state' + description: 'Explain and debug QueryClient, QueryCache, MutationCache, Query, QueryObserver, active versus inactive queries, observer-level options, and subscription behavior.' + type: 'core' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + covers: + - 'QueryClient' + - 'QueryCache' + - 'MutationCache' + - 'Query' + - 'QueryObserver' + - 'active and inactive queries' + - 'observer-level options' + tasks: + - 'Explain why a query did or did not refetch' + - 'Debug active observer count' + - 'Choose QueryClient APIs versus observed hook reads' + failure_modes: + - mistake: 'Treating cache presence as active usage' + mechanism: 'Imperative cache reads do not create observers, so refetch triggers and garbage collection differ from useQuery.' + wrong_pattern: | + const todo = queryClient.getQueryData(['todo', id]) + correct_pattern: | + const todo = useQuery(todoOptions(id)) + source: 'https://tkdodo.eu/blog/inside-react-query' + priority: 'HIGH' + status: 'active' + - mistake: 'Expecting one query to have one option set' + mechanism: 'Several options, including select and staleTime, live on observers and can differ per component.' + wrong_pattern: | + queryClient.getQueryCache().find({ queryKey })?.options.staleTime + correct_pattern: | + queryClient.getQueryCache().find({ queryKey })?.observers.map((observer) => observer.options.staleTime) + source: 'https://tkdodo.eu/blog/inside-react-query' + priority: 'MEDIUM' + status: 'active' + - mistake: 'Expecting inactive queries to refetch like mounted queries' + mechanism: 'Inactive queries are cached but not observed by a mounted component.' + wrong_pattern: | + queryClient.getQueryData(['todos']) + queryClient.invalidateQueries({ queryKey: ['todos'] }) + correct_pattern: | + useQuery(todosOptions()) + queryClient.invalidateQueries({ queryKey: ['todos'], refetchType: 'active' }) + source: 'https://tkdodo.eu/blog/inside-react-query' + priority: 'MEDIUM' + status: 'active' + + - name: 'Handle status checks and errors' + slug: 'handle-status-and-errors' + domain: 'reading-server-state' + description: 'Design loading, stale data, background refetch, error boundary, throwOnError, toast, and validation-error handling without erasing useful cached data.' + type: 'framework' + packages: + - '@tanstack/react-query' + - '@tanstack/query-core' + covers: + - 'status' + - 'fetchStatus' + - 'isPending' + - 'isFetching' + - 'isError with data' + - 'throwOnError' + - 'QueryCache onError' + tasks: + - 'Render stale data after background error' + - 'Route severe errors to Error Boundaries' + - 'Avoid duplicated observer-level error toasts' + failure_modes: + - mistake: 'Hiding stale data on background error' + mechanism: 'A query can have cached data and an error from a later background refetch.' + wrong_pattern: | + if (query.isError) return + if (query.data) return + correct_pattern: | + if (query.data) return + if (query.isError) return + source: 'https://tkdodo.eu/blog/status-checks-in-react-query' + priority: 'HIGH' + status: 'active' + - mistake: 'Sending validation errors to a global boundary' + mechanism: 'Expected 4xx form errors should stay near the form while 5xx errors can go to the boundary.' + wrong_pattern: | + useMutation({ mutationFn: submitForm, throwOnError: true }) + correct_pattern: | + useMutation({ mutationFn: submitForm, throwOnError: (error) => error.status >= 500 }) + source: 'https://tkdodo.eu/blog/react-query-error-handling' + priority: 'HIGH' + status: 'active' + - mistake: 'Duplicated toast notifications per observer' + mechanism: 'Observer-level error callbacks can fire once per observer for the same underlying query.' + wrong_pattern: | + useQuery({ queryKey: ['todos'], queryFn: fetchTodos, onError: toastError }) + correct_pattern: | + new QueryClient({ queryCache: new QueryCache({ onError: toastError }) }) + source: 'https://tkdodo.eu/blog/react-query-error-handling' + priority: 'MEDIUM' + status: 'active' + + - name: 'Query data and forms' + slug: 'query-data-and-forms' + domain: 'writing-server-state' + description: 'Integrate Query data and mutations with forms by choosing initial-only server state, derived server-plus-client state, dirty field behavior, double-submit prevention, and reset-after-invalidation.' + type: 'composition' + packages: + - '@tanstack/react-query' + covers: + - 'form initial values' + - 'dirty client state' + - 'background form updates' + - 'mutation submission state' + - 'reset after invalidation' + tasks: + - 'Initialize form fields from query data' + - 'Keep untouched fields updated from background refetches' + - 'Submit and reset forms after mutation invalidation' + failure_modes: + - mistake: 'Initializing form defaults before query data exists' + mechanism: 'The first render usually has undefined query data.' + wrong_pattern: | + const { data } = useQuery(personOptions(id)) + const [draft, setDraft] = useState(data) + correct_pattern: | + const { data } = useQuery(personOptions(id)) + if (!data) return + return + source: 'https://tkdodo.eu/blog/react-query-and-forms' + priority: 'HIGH' + status: 'active' + - mistake: 'Background refetch overwrites dirty client state' + mechanism: 'Copying server data into form state on every update loses deliberate local edits.' + wrong_pattern: | + useEffect(() => setDraft(personQuery.data), [personQuery.data]) + correct_pattern: | + const shownFirstName = draft.firstName ?? personQuery.data?.firstName ?? '' + source: 'https://tkdodo.eu/blog/deriving-client-state-from-server-state' + priority: 'HIGH' + status: 'active' + - mistake: 'Double submit while mutation is pending' + mechanism: 'Forms can submit duplicate writes unless mutation state disables the action.' + wrong_pattern: | + + correct_pattern: | + + source: 'https://tkdodo.eu/blog/react-query-and-forms' + priority: 'MEDIUM' + status: 'active' + + - name: 'Automatic invalidation after mutations' + slug: 'automatic-invalidation-after-mutations' + domain: 'writing-server-state' + description: 'Create app-level MutationCache invalidation policies with mutationKey scoping, meta invalidation tags, awaited invalidation, and exclusions for static data.' + type: 'composition' + packages: + - '@tanstack/query-core' + - '@tanstack/react-query' + covers: + - 'MutationCache callbacks' + - 'mutationKey invalidation' + - 'mutation meta' + - 'matchQuery' + - 'awaited invalidation' + tasks: + - 'Invalidate related queries after every mutation' + - 'Scope invalidation by mutation key' + - 'Attach explicit invalidation tags through mutation meta' + failure_modes: + - mistake: 'Invalidating the whole app for every mutation' + mechanism: 'A global MutationCache callback without scope can refetch unrelated data.' + wrong_pattern: | + new MutationCache({ onSuccess: () => queryClient.invalidateQueries() }) + correct_pattern: | + new MutationCache({ + onSuccess: (_data, _variables, _context, mutation) => + queryClient.invalidateQueries({ queryKey: mutation.options.mutationKey }), + }) + source: 'https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations' + priority: 'HIGH' + status: 'active' + - mistake: 'Not returning invalidation when pending UI depends on refetch' + mechanism: 'Returning the invalidation promise keeps the mutation pending until related reads are refreshed.' + wrong_pattern: | + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) } + correct_pattern: | + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }) + source: 'docs/framework/react/guides/invalidations-from-mutations.md' + priority: 'HIGH' + status: 'active' + - mistake: 'Refetching data that should be static' + mechanism: 'Data that must ignore broad manual invalidation should opt into v5 static freshness.' + wrong_pattern: | + useQuery({ + queryKey: ['build-info'], + queryFn: fetchBuildInfo, + staleTime: Infinity, + }) + correct_pattern: | + useQuery({ + queryKey: ['build-info'], + queryFn: fetchBuildInfo, + staleTime: 'static', + }) + source: 'https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations' + priority: 'MEDIUM' + status: 'active' + + - name: 'Concurrent optimistic updates' + slug: 'concurrent-optimistic-updates' + domain: 'writing-server-state' + description: 'Handle overlapping optimistic mutations with cancellation, scoped mutation keys, submittedAt identities, per-mutation rollback, filtered cache updates, and guarded invalidation.' + type: 'core' + packages: + - '@tanstack/react-query' + - '@tanstack/query-core' + covers: + - 'useMutationState' + - 'submittedAt' + - 'queryClient.isMutating' + - 'mutationKey filters' + - 'filtered optimistic list updates' + tasks: + - 'Render multiple pending optimistic rows' + - 'Prevent one invalidation from reverting another optimistic write' + - 'Keep filtered lists consistent during optimistic edits' + failure_modes: + - mistake: 'One mutation invalidation reverts another optimistic update' + mechanism: 'An earlier settled mutation can refetch and overwrite a later pending optimistic write.' + wrong_pattern: | + onSettled: () => queryClient.invalidateQueries({ queryKey: ['items'] }) + correct_pattern: | + onSettled: () => { + if (queryClient.isMutating({ mutationKey: ['items'] }) === 1) { + return queryClient.invalidateQueries({ queryKey: ['items'] }) + } + } + source: 'https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query' + priority: 'CRITICAL' + status: 'active' + - mistake: 'Optimistic list update ignores current filters' + mechanism: 'The optimistic cache write must mirror list filtering that the server response would apply.' + wrong_pattern: | + old?.map((item) => item.id === updated.id ? updated : item) + correct_pattern: | + old?.map((item) => item.id === updated.id ? updated : item).filter((item) => matchesFilters(item, filters)) + source: 'https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query' + priority: 'HIGH' + status: 'active' + - mistake: 'Pending optimistic rows share unstable keys' + mechanism: 'Concurrent variables can be equal; submittedAt identifies each mutation attempt.' + wrong_pattern: | + variables.map((title) =>
  • {title}
  • ) + correct_pattern: | + pending.map((mutation) =>
  • {mutation.variables.title}
  • ) + source: 'docs/framework/react/guides/optimistic-updates.md' + priority: 'HIGH' + status: 'active' + + - name: 'Selectors and derived state' + slug: 'selectors-and-derived-state' + domain: 'shaping-cache-and-render-behavior' + description: 'Use select, stable selectors, structural sharing, queryOptions composition, and render-time derivation to subscribe to precise server-state slices without effect-based syncing.' + type: 'core' + packages: + - '@tanstack/react-query' + - '@tanstack/query-core' + covers: + - 'select' + - 'selector identity' + - 'structural sharing of selected data' + - 'derived client state' + - 'usage-site select composition' + tasks: + - 'Subscribe to one field from a larger response' + - 'Memoize expensive select transforms' + - 'Replace useEffect syncing with derived values' + failure_modes: + - mistake: 'Syncing derived state through an effect' + mechanism: 'Server state changes can invalidate client selections without needing a side-effect sync.' + wrong_pattern: | + useEffect(() => syncSelectedUser(users), [users]) + correct_pattern: | + const selectedUser = users?.find((user) => user.id === selectedUserId) + source: 'https://tkdodo.eu/blog/deriving-client-state-from-server-state' + priority: 'HIGH' + status: 'active' + - mistake: 'Inline expensive select reruns on unrelated renders' + mechanism: 'Query reruns select when data changes or the select function identity changes.' + wrong_pattern: | + useSuspenseQuery({ ...productsOptions(filters), select: (data) => expensiveTransform(data) }) + correct_pattern: | + const selectProducts = (data) => expensiveTransform(data) + useSuspenseQuery({ ...productsOptions(filters), select: selectProducts }) + source: 'https://tkdodo.eu/blog/react-query-selectors-supercharged' + priority: 'MEDIUM' + status: 'active' + - mistake: 'Using select to throw domain errors' + mechanism: 'select transforms successful data; query errors should be produced by queryFn.' + wrong_pattern: | + useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select: (todos) => { + if (!todos.length) throw new Error('empty') + return todos + }}) + correct_pattern: | + useQuery({ queryKey: ['todos'], queryFn: async () => { + const todos = await fetchTodos() + if (!todos.length) throw new Error('empty') + return todos + }}) + source: 'docs/framework/react/guides/render-optimizations.md' + priority: 'MEDIUM' + status: 'active' + +tensions: + - name: 'Declarative cache versus imperative fetch control' + skills: + [ + 'coordinate-dependent-parallel-disabled-and-background-queries', + 'fetch-and-observe-queries', + ] + description: 'Disabled queries and manual refetch feel simple but opt out of the cache lifecycle that makes Query useful.' + implication: 'Agents often generate button-driven refetch code instead of modeling dependencies in queryKey and enabled.' + - name: 'SSR freshness versus user data isolation' + skills: + [ + 'setup-query-client-and-providers', + 'compose-query-with-tanstack-router-and-start', + 'ssr-hydration-and-streaming', + 'tune-defaults-freshness-retries-and-refetching', + ] + description: 'SSR needs per-request clients and nonzero staleTime, but TanStack Router and Start also need the QueryClient placed in route context so loaders, hydration, and streaming share one request-local cache.' + implication: 'Agents either create a global SSR client or create a fresh browser client during every render.' + - name: 'Router-owned loading versus component-only queries' + skills: + [ + 'prefetch-and-remove-request-waterfalls', + 'compose-query-with-tanstack-router-and-start', + 'ssr-hydration-and-streaming', + 'understand-query-internals-and-observers', + ] + description: 'TanStack Router and Start know the target route before render, but route loaders should prime Query cache rather than replace active Query observers.' + implication: 'Agents that start from Next, component-only examples, or useLoaderData recreate waterfalls and bypass Query observer behavior.' + - name: 'Optimistic responsiveness versus server truth' + skills: + [ + 'implement-optimistic-updates-and-cache-writes', + 'concurrent-optimistic-updates', + 'write-mutations-and-invalidate-related-queries', + 'persist-offline-and-restore-caches', + ] + description: 'Optimistic cache writes improve UX but need cancellation, rollback, invalidation, and persistence awareness.' + implication: 'Agents write optimistic data and forget that refetch, reload, or offline restore can overwrite or lose it.' + - name: 'Reusable abstractions versus type erasure' + skills: + [ + 'design-query-keys-and-options', + 'build-query-abstractions', + 'selectors-and-derived-state', + ] + description: 'Query abstraction should preserve the full options surface and select inference instead of hiding it behind wide custom hook wrappers.' + implication: 'Agents often generate custom hooks that cannot run in loaders, cannot compose with Suspense, and break select typing.' + - name: 'Automatic invalidation versus scoped freshness' + skills: + [ + 'automatic-invalidation-after-mutations', + 'write-mutations-and-invalidate-related-queries', + 'tune-defaults-freshness-retries-and-refetching', + ] + description: 'Global mutation invalidation reduces boilerplate but can refetch unrelated or static data unless mutation keys, meta, or predicates scope it.' + implication: 'Agents choose either no invalidation or invalidateQueries() everywhere.' + - name: 'Form client ownership versus background server freshness' + skills: + [ + 'query-data-and-forms', + 'selectors-and-derived-state', + 'write-mutations-and-invalidate-related-queries', + ] + description: 'Forms may intentionally copy server state as initial values, but collaborative or long-lived forms need derived server-plus-client state.' + implication: 'Agents overwrite dirty fields with background refetches or disable useful background updates without acknowledging the tradeoff.' + - name: 'Framework idioms versus copied React examples' + skills: + [ + 'use-framework-adapter-reactivity', + 'design-query-keys-and-options', + 'ssr-hydration-and-streaming', + ] + description: 'Adapters share core concepts but differ in how reactivity and SSR hooks are expressed.' + implication: 'Agents port React examples to Vue, Svelte, Angular, or Lit without preserving reactive inputs or provider boundaries.' + +cross_references: + - from: 'setup-query-client-and-providers' + to: 'ssr-hydration-and-streaming' + reason: 'Client creation rules change between browser app lifecycle and per-request server lifecycle.' + - from: 'design-query-keys-and-options' + to: 'enforce-query-best-practices-with-eslint' + reason: 'The lint rules encode key dependency and reusable options mistakes.' + - from: 'design-query-keys-and-options' + to: 'build-query-abstractions' + reason: 'Options factories are the base abstraction for stable keys, query functions, and typed QueryClient usage.' + - from: 'fetch-and-observe-queries' + to: 'tune-defaults-freshness-retries-and-refetching' + reason: 'Status interpretation depends on stale, retry, focus, online, and network settings.' + - from: 'fetch-and-observe-queries' + to: 'handle-status-and-errors' + reason: 'Status, fetchStatus, background errors, and throwOnError determine what UI should render.' + - from: 'fetch-and-observe-queries' + to: 'understand-query-internals-and-observers' + reason: 'Hook reads create QueryObservers, while QueryClient reads only inspect cache state.' + - from: 'write-mutations-and-invalidate-related-queries' + to: 'implement-optimistic-updates-and-cache-writes' + reason: 'Optimistic writes are mutation side effects that still need invalidation and rollback.' + - from: 'write-mutations-and-invalidate-related-queries' + to: 'automatic-invalidation-after-mutations' + reason: 'App-level mutation policies build on mutation keys, MutationCache callbacks, and awaited invalidation.' + - from: 'implement-optimistic-updates-and-cache-writes' + to: 'cancel-queries-and-consume-abort-signals' + reason: 'Optimistic writes commonly need cancellation to avoid stale refetch overwrites.' + - from: 'implement-optimistic-updates-and-cache-writes' + to: 'concurrent-optimistic-updates' + reason: 'Concurrent optimistic writes add submittedAt identity, scoped mutation keys, and guarded invalidation.' + - from: 'seed-placeholder-select-and-transform-data' + to: 'ssr-hydration-and-streaming' + reason: 'initialData is a quick SSR path with tradeoffs compared to dehydration.' + - from: 'seed-placeholder-select-and-transform-data' + to: 'selectors-and-derived-state' + reason: 'select is both a transformation hook and a fine-grained subscription mechanism.' + - from: 'selectors-and-derived-state' + to: 'query-data-and-forms' + reason: 'Forms often need derived server-plus-client state instead of effect-based synchronization.' + - from: 'build-query-abstractions' + to: 'selectors-and-derived-state' + reason: 'Abstractions must preserve usage-site select inference and stable selector composition.' + - from: 'prefetch-and-remove-request-waterfalls' + to: 'compose-query-with-tanstack-router-and-start' + reason: 'TanStack Router and Start loaders are the highest-priority route prefetch surface in the TanStack ecosystem.' + - from: 'compose-query-with-tanstack-router-and-start' + to: 'ssr-hydration-and-streaming' + reason: 'Router SSR Query integration owns the QueryClient cache handoff, streamed query results, and route loader prefetch during SSR.' + - from: 'compose-query-with-tanstack-router-and-start' + to: 'use-suspense-and-error-boundaries' + reason: 'Suspense queries and loader prefetches participate differently in Router SSR and streaming than plain useQuery.' + - from: 'shape-data-and-render-efficiently' + to: 'enforce-query-best-practices-with-eslint' + reason: 'no-rest-destructuring and no-unstable-deps protect render optimization assumptions.' + - from: 'persist-offline-and-restore-caches' + to: 'tune-defaults-freshness-retries-and-refetching' + reason: 'Persistence maxAge and hydration behavior depend on gcTime, networkMode, and retry settings.' + - from: 'use-framework-adapter-reactivity' + to: 'design-query-keys-and-options' + reason: 'Adapter reactivity usually flows through queryKey and option thunk shapes.' + - from: 'migrate-major-versions-and-codemods' + to: 'design-query-keys-and-options' + reason: 'v5 migration changed all key/function call signatures to object options.' + +gaps: + - skill: 'compose-query-with-tanstack-router-and-start' + question: 'Should this composition skill be generated in Query, Router, Start, or all three with cross-links?' + context: 'The core Query APIs live in this repo, while the first-class Router SSR Query integration and Start guidance live in the Router and Start docs.' + status: 'open' + - skill: 'broadcast-realtime-and-multi-tab-synchronization' + question: 'Which realtime patterns should be blessed as skills versus treated as examples only?' + context: 'Docs include broadcast and examples reference chat/WebSockets, while TKDodo has a WebSockets article, but repo docs do not define a first-class realtime API.' + status: 'open' + - skill: 'use-framework-adapter-reactivity' + question: 'Should adapter skills be generated as one cross-framework skill with references, or one package-local skill per adapter?' + context: 'Intent monorepo generation writes skills inside packages, but the docs intentionally share most guides across adapters.' + status: 'open' + - skill: 'ssr-hydration-and-streaming' + question: 'Should experimental ReactQueryStreamedHydration and streamedQuery be excluded from generated skills or marked experimental with version-lock guidance?' + context: 'Docs mark these APIs experimental; user asked to cover all ecosystem packages, but Intent asks experimental features to be excluded unless ready.' + status: 'open' + - skill: 'migrate-major-versions-and-codemods' + question: 'How far back should generated migration skills go for agents: v3, v4, v5, Svelte v6, or only currently common legacy mistakes?' + context: 'Docs include multiple migration guides, and agents trained on older code frequently emit removed overloads.' + status: 'open' + - skill: 'enforce-query-best-practices-with-eslint' + question: 'Should ESLint skills be React-only or documented as generally useful for Query codebases where the rule supports the adapter?' + context: 'The package is named generically, but many examples and rules are React-hook oriented.' + status: 'open' diff --git a/_artifacts/skill_spec.md b/_artifacts/skill_spec.md new file mode 100644 index 00000000000..5882bfaaa51 --- /dev/null +++ b/_artifacts/skill_spec.md @@ -0,0 +1,189 @@ +# TanStack Query - Skill Spec + +TanStack Query manages server state through a framework-agnostic cache, observers, framework adapters, persistence utilities, devtools, lint rules, and migration tooling. This draft follows the published docs order in `docs/config.json` and treats TKDodo's blog order as an external priority signal. + +TanStack Start and TanStack Router are first-class composition targets here, especially for loaders, SSR hydration, streaming, and cache handoff. Next.js remains important, but it is not the default mental model for TanStack-owned routing. + +The maintainer skipped live interviews, so unresolved judgment calls remain in the gaps section. + +## Domains + +| Domain | Description | Skills | +| ----------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Bootstrapping query clients | Creating stable clients and provider context | setup-query-client-and-providers | +| Reading server state | Declaring reads, query identity, observers, status, and reusable abstractions | design-query-keys-and-options, build-query-abstractions, understand-query-internals-and-observers, fetch-and-observe-queries, handle-status-and-errors | +| Coordinating query execution | Dependency, freshness, retries, pagination, prefetch | tune-defaults-freshness-retries-and-refetching, coordinate-dependent-parallel-disabled-and-background-queries, paginate-and-build-infinite-queries | +| Writing server state | Mutations, invalidation, optimistic writes, forms, cancellation | write-mutations-and-invalidate-related-queries, automatic-invalidation-after-mutations, implement-optimistic-updates-and-cache-writes, concurrent-optimistic-updates, query-data-and-forms, cancel-queries-and-consume-abort-signals | +| Shaping cache and render behavior | Seeded data, placeholders, selectors, derived state, render performance | seed-placeholder-select-and-transform-data, selectors-and-derived-state, shape-data-and-render-efficiently | +| Rendering across environments | SSR, hydration, Suspense, streaming, routers | prefetch-and-remove-request-waterfalls, compose-query-with-tanstack-router-and-start, ssr-hydration-and-streaming, use-suspense-and-error-boundaries | +| Persisting and synchronizing caches | Persistence, offline, multi-tab, realtime | persist-offline-and-restore-caches, broadcast-realtime-and-multi-tab-synchronization | +| Framework adapter idioms | Reactivity and provider rules across adapters | use-framework-adapter-reactivity | +| Operational quality | Devtools, testing, migration, linting | debug-with-devtools, test-query-code, migrate-major-versions-and-codemods, enforce-query-best-practices-with-eslint | + +## Skill Inventory + +| Skill | Type | Domain | What it covers | Failure modes | +| ---------------------------------------------------------------- | ----------- | ----------------------------------- | --------------------------------------------------------------------------------------- | ------------- | +| Set up QueryClient and providers | lifecycle | Bootstrapping query clients | QueryClient, providers, SSR isolation, Lit fallback | 3 | +| Design query keys and reusable options | core | Reading server state | queryKey, queryFn, queryOptions, skipToken | 3 | +| Build Query abstractions | core | Reading server state | queryOptions factories, custom hooks, usage-site composition, TypeScript inference | 3 | +| Understand Query internals and observers | core | Reading server state | QueryClient, QueryCache, QueryObserver, active/inactive queries | 3 | +| Fetch and observe queries | core | Reading server state | useQuery/createQuery/injectQuery, status, fetchStatus | 3 | +| Handle status checks and errors | framework | Reading server state | status, fetchStatus, background errors, throwOnError, cache-level callbacks | 3 | +| Tune defaults, freshness, retries, and refetching | core | Coordinating query execution | staleTime, gcTime, retry, refetch triggers, networkMode | 3 | +| Coordinate dependent, parallel, disabled, and background queries | core | Coordinating query execution | useQueries, enabled, skipToken, background fetching | 3 | +| Paginate and build infinite queries | core | Coordinating query execution | pagination, infinite queries, maxPages | 3 | +| Write mutations and invalidate related queries | core | Writing server state | mutations, invalidation, mutation state | 3 | +| Automatic invalidation after mutations | composition | Writing server state | MutationCache callbacks, mutationKey invalidation, meta tags, scoped invalidation | 3 | +| Implement optimistic updates and cache writes | core | Writing server state | onMutate, rollback, setQueryData | 3 | +| Concurrent optimistic updates | core | Writing server state | submittedAt, mutationKey filters, isMutating guards, filtered optimistic lists | 3 | +| Query data and forms | composition | Writing server state | initial form state, dirty fields, derived server/client state, reset after invalidation | 3 | +| Cancel queries and consume AbortSignals | core | Writing server state | AbortSignal, cancelQueries, CancelledError | 3 | +| Seed, placeholder, select, and transform data | core | Shaping cache and render behavior | initialData, placeholderData, select | 3 | +| Selectors and derived state | core | Shaping cache and render behavior | select, selector identity, structural sharing, render-time derivation | 3 | +| Shape data and render efficiently | framework | Shaping cache and render behavior | structural sharing, tracked props, immutable data | 3 | +| Prefetch and remove request waterfalls | lifecycle | Rendering across environments | prefetchQuery, router loaders, waterfalls | 3 | +| Compose TanStack Query with TanStack Router and Start | composition | Rendering across environments | router context, loaders, `@tanstack/react-router-ssr-query`, Start SSR/streaming | 3 | +| SSR, hydration, and streaming | lifecycle | Rendering across environments | dehydrate/hydrate, Start, Router SSR Query, RSC, SvelteKit, Nuxt | 3 | +| Use Suspense and error boundaries | framework | Rendering across environments | suspense hooks, QueryErrorResetBoundary, React.use | 3 | +| Persist offline and restore caches | composition | Persisting and synchronizing caches | persistQueryClient, persisters, restore races | 3 | +| Broadcast, realtime, and multi-tab synchronization | composition | Persisting and synchronizing caches | broadcastQueryClient, WebSockets, realtime invalidation | 3 | +| Use framework adapter reactivity | framework | Framework adapter idioms | Vue refs, Svelte runes, Angular signals, Lit controllers | 3 | +| Debug with devtools | framework | Operational quality | framework devtools, embedded panels, production imports | 3 | +| Test query code | lifecycle | Operational quality | test clients, retry false, cache isolation | 3 | +| Migrate major versions and codemods | lifecycle | Operational quality | v5 object syntax, renamed options, Svelte v6 | 3 | +| Enforce Query best practices with ESLint | composition | Operational quality | recommended configs and Query-specific rules | 3 | + +## Failure Mode Inventory + +| Skill | High-priority examples | +| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| setup-query-client-and-providers | New client on every render; shared SSR cache between users; ambiguous Lit fallback client | +| design-query-keys-and-options | Missing variables in query key; separated key/queryFn drift; skipToken inside suspense query | +| build-query-abstractions | Custom hook is the only abstraction; wide UseQueryOptions wrapper breaks inference; wrapper hides Query result state | +| understand-query-internals-and-observers | Cache presence treated as active usage; observer-level options ignored; inactive queries expected to refetch like mounted queries | +| fetch-and-observe-queries | Void query function; only checking pending for offline state; using queries for writes | +| handle-status-and-errors | Stale data hidden on background error; validation errors sent to global boundary; duplicate toasts per observer | +| tune-defaults-freshness-retries-and-refetching | Confusing gcTime with freshness; static staleTime blocks invalidation; tests hang on retries | +| coordinate-dependent-parallel-disabled-and-background-queries | Imperative disabled query; duplicate useQueries keys; full-page spinner on background refetch | +| paginate-and-build-infinite-queries | Missing initialPageParam; overlapping infinite fetches; broken pages/pageParams shape | +| write-mutations-and-invalidate-related-queries | Multiple mutate args; per-call callbacks after unmount; not awaiting invalidation | +| automatic-invalidation-after-mutations | Invalidating whole app for every mutation; not returning invalidation; refetching data that should be static | +| implement-optimistic-updates-and-cache-writes | Optimistic write without cancellation; in-place mutation; persisted persister loses optimistic write | +| concurrent-optimistic-updates | One mutation invalidation reverts another optimistic write; filtered list mismatch; unstable pending row keys | +| query-data-and-forms | Form defaults initialized before query data; background refetch overwrites dirty state; double submit while pending | +| cancel-queries-and-consume-abort-signals | Ignoring AbortSignal; assuming unmount cancels; Suspense cancellation expected | +| seed-placeholder-select-and-transform-data | initialData overwrite assumption; v4 keepPreviousData option; throwing in select | +| selectors-and-derived-state | Effect-based derived-state sync; inline expensive select; throwing from select | +| shape-data-and-render-efficiently | Rest destructuring; query result object in hook deps; Vue v-model on immutable result | +| prefetch-and-remove-request-waterfalls | prefetchQuery expected to return data; staleTime only on prefetch; Suspense prefetch in effect | +| compose-query-with-tanstack-router-and-start | Component-only route waterfall; hand-rolled Router hydration; Next.js mental model copied into Start | +| ssr-hydration-and-streaming | RSC renders fetched data twice; suspense query not prefetched; SvelteKit query runs after response | +| use-suspense-and-error-boundaries | Disabled suspense query; missing reset boundary; query.promise without flag | +| persist-offline-and-restore-caches | gcTime shorter than maxAge; rendering before restore; missing default mutationFn | +| broadcast-realtime-and-multi-tab-synchronization | Unlocked experimental broadcast; over-normalized realtime writes; broadcast on server | +| use-framework-adapter-reactivity | Vue ref unwrapped; Svelte store syntax in v6; Angular Observable returned directly | +| debug-with-devtools | Eager production devtools; mock offline misconception; devtools outside provider | +| test-query-code | Shared test client; retry backoff in tests; asserting before async success | +| migrate-major-versions-and-codemods | v4 overload syntax; removed query callbacks; cacheTime in v5 | +| enforce-query-best-practices-with-eslint | Strict rule not enabled; infinite option order; mutation option order | + +## Tensions + +| Tension | Skills | Agent implication | +| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| Declarative cache versus imperative fetch control | coordinate-dependent-parallel-disabled-and-background-queries <-> fetch-and-observe-queries | Agents generate manual refetch flows instead of key and enabled-driven queries. | +| SSR freshness versus user data isolation | setup-query-client-and-providers <-> compose-query-with-tanstack-router-and-start <-> ssr-hydration-and-streaming <-> tune-defaults-freshness-retries-and-refetching | Agents leak server cache or double-fetch after hydration. | +| Router-owned loading versus component-only queries | prefetch-and-remove-request-waterfalls <-> compose-query-with-tanstack-router-and-start <-> ssr-hydration-and-streaming <-> understand-query-internals-and-observers | Agents recreate waterfalls, use loader data as server-state ownership, and bypass Query observer behavior. | +| Optimistic responsiveness versus server truth | implement-optimistic-updates-and-cache-writes <-> concurrent-optimistic-updates <-> write-mutations-and-invalidate-related-queries <-> persist-offline-and-restore-caches | Agents write optimistic data without cancellation, rollback, invalidation, concurrency guards, or persistence awareness. | +| Reusable abstractions versus type erasure | design-query-keys-and-options <-> build-query-abstractions <-> selectors-and-derived-state | Agents generate custom hooks or wide options wrappers that cannot run in loaders and break select inference. | +| Automatic invalidation versus scoped freshness | automatic-invalidation-after-mutations <-> write-mutations-and-invalidate-related-queries <-> tune-defaults-freshness-retries-and-refetching | Agents choose no invalidation or global invalidation without mutation keys, meta, or static-data exclusions. | +| Form client ownership versus background server freshness | query-data-and-forms <-> selectors-and-derived-state <-> write-mutations-and-invalidate-related-queries | Agents overwrite dirty form fields with refetches or disable useful background updates without acknowledging the tradeoff. | +| Framework idioms versus copied React examples | use-framework-adapter-reactivity <-> design-query-keys-and-options <-> ssr-hydration-and-streaming | Agents port React examples without preserving adapter reactivity. | + +## Cross-References + +| From | To | Reason | +| ---------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| setup-query-client-and-providers | ssr-hydration-and-streaming | Client lifetime differs between browser and server. | +| design-query-keys-and-options | enforce-query-best-practices-with-eslint | Lint rules encode key and options mistakes. | +| design-query-keys-and-options | build-query-abstractions | Options factories are the base abstraction for reusable keys and query functions. | +| fetch-and-observe-queries | tune-defaults-freshness-retries-and-refetching | Status behavior depends on defaults and network mode. | +| fetch-and-observe-queries | handle-status-and-errors | Status, fetchStatus, background errors, and throwOnError drive UI behavior. | +| fetch-and-observe-queries | understand-query-internals-and-observers | Hooks create QueryObservers, while QueryClient reads only inspect cache. | +| write-mutations-and-invalidate-related-queries | implement-optimistic-updates-and-cache-writes | Optimistic updates are mutation side effects. | +| write-mutations-and-invalidate-related-queries | automatic-invalidation-after-mutations | App-level invalidation policies build on mutation keys and MutationCache callbacks. | +| implement-optimistic-updates-and-cache-writes | cancel-queries-and-consume-abort-signals | Optimistic writes often need cancellation first. | +| implement-optimistic-updates-and-cache-writes | concurrent-optimistic-updates | Concurrent writes need submittedAt identity, mutationKey scoping, and guarded invalidation. | +| seed-placeholder-select-and-transform-data | ssr-hydration-and-streaming | initialData has SSR tradeoffs compared to hydration. | +| seed-placeholder-select-and-transform-data | selectors-and-derived-state | select is both a transformation hook and a fine-grained subscription mechanism. | +| selectors-and-derived-state | query-data-and-forms | Forms often need derived server-plus-client state instead of effect-based synchronization. | +| build-query-abstractions | selectors-and-derived-state | Query abstractions should preserve usage-site select inference. | +| prefetch-and-remove-request-waterfalls | compose-query-with-tanstack-router-and-start | Router and Start loaders are the highest-priority route prefetch surface in the TanStack ecosystem. | +| compose-query-with-tanstack-router-and-start | ssr-hydration-and-streaming | Router SSR Query integration owns cache handoff and streamed query results. | +| compose-query-with-tanstack-router-and-start | use-suspense-and-error-boundaries | Suspense queries and loader prefetches participate in Router SSR/streaming differently than plain useQuery. | +| shape-data-and-render-efficiently | enforce-query-best-practices-with-eslint | Lint rules protect tracked render behavior. | +| persist-offline-and-restore-caches | tune-defaults-freshness-retries-and-refetching | Persistence depends on gcTime, networkMode, and retry. | +| use-framework-adapter-reactivity | design-query-keys-and-options | Adapter reactivity usually flows through keys and option thunks. | +| migrate-major-versions-and-codemods | design-query-keys-and-options | v5 changed hooks and client methods to object options. | + +## Subsystems & Reference Candidates + +| Skill | Subsystems | Reference candidates | +| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | +| design-query-keys-and-options | framework option helpers | queryOptions/infiniteQueryOptions/mutationOptions per adapter | +| build-query-abstractions | queryOptions factories, custom hook wrappers, TypeScript inference | wrong/correct abstraction examples from TKDodo #31 | +| understand-query-internals-and-observers | QueryClient, QueryCache, Query, QueryObserver, MutationCache | active/inactive observer mental model | +| handle-status-and-errors | status, fetchStatus, throwOnError, QueryCache callbacks | stale-data-first status recipes and background error toast policies | +| query-data-and-forms | form initial values, dirty state, mutation submit/reset | initial-only versus derived-state form recipes | +| automatic-invalidation-after-mutations | MutationCache, mutationKey, meta, matchQuery | global invalidation policies and static-data exclusions | +| concurrent-optimistic-updates | submittedAt, mutationKey, isMutating, useMutationState | concurrent rollback and over-invalidation recipes | +| selectors-and-derived-state | select, stable selectors, structural sharing, derived client state | selector identity and render-time derivation recipes | +| compose-query-with-tanstack-router-and-start | TanStack Start, TanStack Router, Router SSR Query integration | route loader, router context QueryClient, setupRouterSsrQueryIntegration, Start server function references | +| ssr-hydration-and-streaming | TanStack Start, TanStack Router SSR Query, Next pages, Next app router, Nuxt, SvelteKit, SolidStart, Lit SSR | dehydrate/hydrate recipes by framework | +| persist-offline-and-restore-caches | async persister, sync persister, fine-grained persister, framework providers | persister storage and retry interfaces | +| use-framework-adapter-reactivity | React/Preact, Vue, Solid, Svelte, Angular, Lit | adapter mapping tables and reactivity rules | +| debug-with-devtools | React, Preact, Vue, Solid, Svelte, Angular | import paths and production-lazy setup | +| enforce-query-best-practices-with-eslint | recommended, recommended-strict, custom rules | rule-specific wrong/correct examples | + +## Recommended Skill File Structure + +- Core skills: `design-query-keys-and-options`, `build-query-abstractions`, `understand-query-internals-and-observers`, `fetch-and-observe-queries`, `tune-defaults-freshness-retries-and-refetching`, `coordinate-dependent-parallel-disabled-and-background-queries`, `paginate-and-build-infinite-queries`, `write-mutations-and-invalidate-related-queries`, `implement-optimistic-updates-and-cache-writes`, `concurrent-optimistic-updates`, `cancel-queries-and-consume-abort-signals`, `seed-placeholder-select-and-transform-data`, `selectors-and-derived-state`. +- Framework skills: `handle-status-and-errors`, `use-framework-adapter-reactivity`, `shape-data-and-render-efficiently`, `use-suspense-and-error-boundaries`, `debug-with-devtools`. +- Lifecycle skills: `setup-query-client-and-providers`, `prefetch-and-remove-request-waterfalls`, `ssr-hydration-and-streaming`, `test-query-code`, `migrate-major-versions-and-codemods`. +- Composition skills: `compose-query-with-tanstack-router-and-start`, `query-data-and-forms`, `automatic-invalidation-after-mutations`, `persist-offline-and-restore-caches`, `broadcast-realtime-and-multi-tab-synchronization`, `enforce-query-best-practices-with-eslint`. +- Reference files: per-adapter API mapping, SSR recipes, persistence recipes, ESLint rule examples, migration wrong/correct pairs, TKDodo source index. + +## Composition Opportunities + +| Library | Integration points | Composition skill needed? | +| ----------------------------------- | -------------------------------------------------------------------------- | ------------------------- | +| TanStack Start | routes, server functions, SSR, streaming, Query integration via Router | yes | +| TanStack Router | route loaders, prefetch, cache handoff, `@tanstack/react-router-ssr-query` | yes | +| React Router | loaders and query cache seeding | yes | +| Next.js | app router, pages router, streaming | yes | +| Remix | loaders and hydration | yes | +| Nuxt | Vue Query SSR plugin and onServerPrefetch | yes | +| SvelteKit | load functions, browser gating, prefetchQuery | yes | +| SolidStart | SSR and route-level prefetch | yes | +| Angular HttpClient | Observable to Promise conversion | yes | +| GraphQL clients | queryFn and cancellation integration | maybe | +| WebSocket clients | event-driven invalidation and cache updates | maybe | +| AsyncStorage/localStorage/IndexedDB | persister storage backends | yes | + +## Source Priority Notes + +- In-repo docs read: all 493 markdown files under `docs/`, plus root README and package manifests. +- Published docs priority source: `docs/config.json`, especially the `Guides & Concepts` order. +- TKDodo priority source: React Query series order on `https://tkdodo.eu/blog/tan-stack-router-and-query` now lists 33 parts, with "TanStack Router and Query" as #32. Cross-checked gaps are recorded in `_artifacts/tkdodo_cross_check.md`. + +## Remaining Gaps + +| Skill | Question | Status | +| ------------------------------------------------ | -------------------------------------------------------------------------------------------------- | ------ | +| compose-query-with-tanstack-router-and-start | Should this composition skill be generated in Query, Router, Start, or all three with cross-links? | open | +| broadcast-realtime-and-multi-tab-synchronization | Which realtime patterns should be blessed as skills versus examples only? | open | +| use-framework-adapter-reactivity | Cross-framework skill with references, or package-local adapter skills? | open | +| ssr-hydration-and-streaming | Include or exclude experimental streamed APIs? | open | +| migrate-major-versions-and-codemods | How far back should generated migration skills go? | open | +| enforce-query-best-practices-with-eslint | Should ESLint skills be React-only or cross-adapter where rules apply? | open | diff --git a/_artifacts/skill_tree.yaml b/_artifacts/skill_tree.yaml new file mode 100644 index 00000000000..9bd74ffe4f9 --- /dev/null +++ b/_artifacts/skill_tree.yaml @@ -0,0 +1,704 @@ +# skill_tree.yaml +# Generated by skill-tree-generator +# Library: TanStack Query +# Date: 2026-06-03 +# Status: draft + +library: + name: 'TanStack Query' + version: '5.101.0' + repository: 'https://github.com/TanStack/query' + description: 'Framework adapters and core utilities for fetching, caching, synchronizing, and updating server state.' +generated_from: + domain_map: '_artifacts/domain_map.yaml' + skill_spec: '_artifacts/skill_spec.md' +generated_at: '2026-06-03T21:41:21Z' + +skills: + - name: 'Set up QueryClient and providers' + slug: 'setup-query-client-and-providers' + type: 'lifecycle' + domain: 'bootstrapping-query-clients' + path: 'packages/query-intent/skills/lifecycle/setup-query-client-and-providers/SKILL.md' + package: 'packages/query-intent' + description: 'Create stable QueryClient instances, wire framework providers, and avoid cache leaks across renders or SSR requests.' + sources: + - 'TanStack/query:docs/framework/react/quick-start.md' + - 'TanStack/query:docs/framework/react/reference/QueryClientProvider.md' + - 'TanStack/query:docs/eslint/stable-query-client.md' + - 'TanStack/query:docs/framework/vue/guides/custom-client.md' + - 'TanStack/query:docs/framework/angular/overview.md' + - 'TanStack/query:docs/framework/lit/guides/reactive-controllers-vs-hooks.md' + subsystems: + - 'React' + - 'Preact' + - 'Vue' + - 'Solid' + - 'Svelte' + - 'Angular' + - 'Lit' + references: + - 'references/providers-by-adapter.md' + - 'references/ssr-client-lifecycle.md' + + - name: 'Design query keys and reusable options' + slug: 'design-query-keys-and-options' + type: 'core' + domain: 'reading-server-state' + path: 'packages/query-intent/skills/core/design-query-keys-and-options/SKILL.md' + package: 'packages/query-intent' + description: 'Design complete query keys and reusable options objects that keep query identity, query functions, and TypeScript inference together.' + sources: + - 'TanStack/query:docs/framework/react/guides/query-keys.md' + - 'TanStack/query:docs/framework/react/guides/query-options.md' + - 'TanStack/query:docs/framework/react/typescript.md' + - 'TanStack/query:docs/eslint/exhaustive-deps.md' + - 'TanStack/query:docs/eslint/prefer-query-options.md' + subsystems: + - 'queryOptions' + - 'infiniteQueryOptions' + - 'mutationOptions' + - 'skipToken' + references: + - 'references/options-api-by-adapter.md' + - 'references/query-key-factories.md' + + - name: 'Build Query abstractions' + slug: 'build-query-abstractions' + type: 'core' + domain: 'reading-server-state' + path: 'packages/query-intent/skills/core/build-query-abstractions/SKILL.md' + package: 'packages/query-intent' + description: 'Create queryOptions factories and custom hooks that preserve TypeScript inference and work across hooks, loaders, prefetches, and QueryClient calls.' + requires: + - 'design-query-keys-and-options' + sources: + - 'https://tkdodo.eu/blog/creating-query-abstractions' + - 'https://tkdodo.eu/blog/the-query-options-api' + - 'TanStack/query:docs/framework/react/guides/query-options.md' + - 'TanStack/query:docs/framework/react/typescript.md' + subsystems: + - 'queryOptions' + - 'custom hooks' + - 'TypeScript inference' + references: + - 'references/query-abstractions.md' + + - name: 'Understand Query internals and observers' + slug: 'understand-query-internals-and-observers' + type: 'core' + domain: 'reading-server-state' + path: 'packages/query-intent/skills/core/understand-query-internals-and-observers/SKILL.md' + package: 'packages/query-intent' + description: 'Explain QueryClient, QueryCache, MutationCache, QueryObserver, active and inactive queries, and observer-level option behavior.' + requires: + - 'fetch-and-observe-queries' + sources: + - 'https://tkdodo.eu/blog/inside-react-query' + - 'https://tkdodo.eu/blog/react-query-selectors-supercharged' + - 'TanStack/query:docs/reference/QueryClient.md' + - 'TanStack/query:docs/reference/QueryCache.md' + - 'TanStack/query:docs/reference/QueryObserver.md' + - 'TanStack/query:docs/reference/MutationCache.md' + subsystems: + - 'QueryClient' + - 'QueryCache' + - 'QueryObserver' + - 'active queries' + - 'inactive queries' + references: + - 'references/query-internals-and-observers.md' + + - name: 'Fetch and observe queries' + slug: 'fetch-and-observe-queries' + type: 'core' + domain: 'reading-server-state' + path: 'packages/query-intent/skills/core/fetch-and-observe-queries/SKILL.md' + package: 'packages/query-intent' + description: 'Read server state with query hooks, observers, and QueryClient fetch methods while interpreting status and fetchStatus correctly.' + requires: + - 'setup-query-client-and-providers' + - 'design-query-keys-and-options' + sources: + - 'TanStack/query:docs/framework/react/guides/queries.md' + - 'TanStack/query:docs/framework/react/guides/query-functions.md' + - 'TanStack/query:docs/reference/QueryObserver.md' + - 'TanStack/query:docs/reference/QueryClient.md' + - 'TanStack/query:docs/eslint/no-void-query-fn.md' + subsystems: + - 'useQuery' + - 'useQueries' + - 'QueryObserver' + - 'QueriesObserver' + - 'QueryClient' + references: + - 'references/query-observation-by-adapter.md' + - 'references/status-and-fetch-status.md' + + - name: 'Handle status checks and errors' + slug: 'handle-status-and-errors' + type: 'framework' + domain: 'reading-server-state' + path: 'packages/query-intent/skills/framework/handle-status-and-errors/SKILL.md' + package: 'packages/query-intent' + description: 'Design loading, stale data, background refetch error, throwOnError, Error Boundary, toast, and validation-error flows.' + requires: + - 'fetch-and-observe-queries' + - 'use-suspense-and-error-boundaries' + sources: + - 'https://tkdodo.eu/blog/status-checks-in-react-query' + - 'https://tkdodo.eu/blog/react-query-error-handling' + - 'TanStack/query:docs/framework/react/guides/queries.md' + - 'TanStack/query:docs/framework/react/reference/QueryErrorResetBoundary.md' + subsystems: + - 'status' + - 'fetchStatus' + - 'throwOnError' + - 'QueryCache callbacks' + references: + - 'references/status-and-error-handling.md' + + - name: 'Tune defaults, freshness, retries, and refetching' + slug: 'tune-defaults-freshness-retries-and-refetching' + type: 'core' + domain: 'coordinating-query-execution' + path: 'packages/query-intent/skills/core/tune-defaults-freshness-retries-and-refetching/SKILL.md' + package: 'packages/query-intent' + description: 'Choose staleTime, gcTime, retries, refetch triggers, network mode, and default options without confusing cache lifetime with freshness.' + requires: + - 'fetch-and-observe-queries' + sources: + - 'TanStack/query:docs/framework/react/guides/important-defaults.md' + - 'TanStack/query:docs/framework/react/guides/caching.md' + - 'TanStack/query:docs/framework/react/guides/query-retries.md' + - 'TanStack/query:docs/framework/react/guides/window-focus-refetching.md' + - 'TanStack/query:docs/framework/react/guides/network-mode.md' + - 'TanStack/query:docs/reference/focusManager.md' + - 'TanStack/query:docs/reference/onlineManager.md' + - 'TanStack/query:docs/reference/timeoutManager.md' + references: + - 'references/defaults-and-freshness.md' + - 'references/focus-online-timeout-managers.md' + + - name: 'Coordinate dependent, parallel, disabled, and background queries' + slug: 'coordinate-dependent-parallel-disabled-and-background-queries' + type: 'core' + domain: 'coordinating-query-execution' + path: 'packages/query-intent/skills/core/coordinate-query-execution/SKILL.md' + package: 'packages/query-intent' + description: 'Coordinate query execution with enabled, skipToken, useQueries, background indicators, and dependency-driven keys instead of imperative refetch flows.' + requires: + - 'fetch-and-observe-queries' + - 'tune-defaults-freshness-retries-and-refetching' + sources: + - 'TanStack/query:docs/framework/react/guides/dependent-queries.md' + - 'TanStack/query:docs/framework/react/guides/parallel-queries.md' + - 'TanStack/query:docs/framework/react/guides/disabling-queries.md' + - 'TanStack/query:docs/framework/react/guides/background-fetching-indicators.md' + - 'TanStack/query:docs/framework/react/reference/useQueries.md' + references: + - 'references/query-coordination-patterns.md' + + - name: 'Paginate and build infinite queries' + slug: 'paginate-and-build-infinite-queries' + type: 'core' + domain: 'coordinating-query-execution' + path: 'packages/query-intent/skills/core/paginate-and-build-infinite-queries/SKILL.md' + package: 'packages/query-intent' + description: 'Build paginated and infinite query flows with stable keys, initialPageParam, getNextPageParam, maxPages, and preserved pages/pageParams shape.' + requires: + - 'design-query-keys-and-options' + - 'fetch-and-observe-queries' + sources: + - 'TanStack/query:docs/framework/react/guides/paginated-queries.md' + - 'TanStack/query:docs/framework/react/guides/infinite-queries.md' + - 'TanStack/query:docs/framework/react/reference/useInfiniteQuery.md' + - 'TanStack/query:docs/reference/InfiniteQueryObserver.md' + - 'TanStack/query:docs/eslint/infinite-query-property-order.md' + references: + - 'references/infinite-query-by-adapter.md' + - 'references/pagination-and-lagged-data.md' + + - name: 'Write mutations and invalidate related queries' + slug: 'write-mutations-and-invalidate-related-queries' + type: 'core' + domain: 'writing-server-state' + path: 'packages/query-intent/skills/core/write-mutations-and-invalidate-related-queries/SKILL.md' + package: 'packages/query-intent' + description: 'Write server state with mutations, scoped mutation defaults, mutation state, invalidation, and awaited post-mutation refetching.' + requires: + - 'design-query-keys-and-options' + - 'fetch-and-observe-queries' + sources: + - 'TanStack/query:docs/framework/react/guides/mutations.md' + - 'TanStack/query:docs/framework/react/guides/query-invalidation.md' + - 'TanStack/query:docs/framework/react/guides/invalidations-from-mutations.md' + - 'TanStack/query:docs/framework/react/guides/updates-from-mutation-responses.md' + - 'TanStack/query:docs/framework/react/reference/useMutation.md' + - 'TanStack/query:docs/framework/react/reference/useMutationState.md' + - 'TanStack/query:docs/reference/MutationCache.md' + references: + - 'references/mutation-options-by-adapter.md' + - 'references/invalidation-patterns.md' + + - name: 'Automatic invalidation after mutations' + slug: 'automatic-invalidation-after-mutations' + type: 'composition' + domain: 'writing-server-state' + path: 'packages/query-intent/skills/compositions/automatic-invalidation-after-mutations/SKILL.md' + package: 'packages/query-intent' + description: 'Create MutationCache invalidation policies with mutationKey scoping, mutation meta tags, awaited invalidation, and static-data exclusions.' + requires: + - 'write-mutations-and-invalidate-related-queries' + - 'design-query-keys-and-options' + sources: + - 'https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations' + - 'TanStack/query:docs/framework/react/guides/invalidations-from-mutations.md' + - 'TanStack/query:docs/reference/MutationCache.md' + - 'TanStack/query:docs/reference/QueryClient.md' + subsystems: + - 'MutationCache' + - 'mutationKey' + - 'mutation meta' + - 'matchQuery' + references: + - 'references/automatic-mutation-invalidation.md' + + - name: 'Implement optimistic updates and cache writes' + slug: 'implement-optimistic-updates-and-cache-writes' + type: 'core' + domain: 'writing-server-state' + path: 'packages/query-intent/skills/core/implement-optimistic-updates-and-cache-writes/SKILL.md' + package: 'packages/query-intent' + description: 'Implement optimistic UI and direct cache writes with cancellation, immutable updates, rollback context, invalidation, and offline persistence awareness.' + requires: + - 'write-mutations-and-invalidate-related-queries' + - 'cancel-queries-and-consume-abort-signals' + sources: + - 'TanStack/query:docs/framework/react/guides/optimistic-updates.md' + - 'TanStack/query:docs/framework/react/guides/updates-from-mutation-responses.md' + - 'TanStack/query:docs/framework/react/guides/query-cancellation.md' + - 'TanStack/query:docs/framework/react/plugins/persistQueryClient.md' + references: + - 'references/optimistic-updates.md' + - 'references/cache-write-invariants.md' + + - name: 'Concurrent optimistic updates' + slug: 'concurrent-optimistic-updates' + type: 'core' + domain: 'writing-server-state' + path: 'packages/query-intent/skills/core/concurrent-optimistic-updates/SKILL.md' + package: 'packages/query-intent' + description: 'Handle overlapping optimistic mutations with submittedAt identity, scoped mutationKey filters, isMutating guards, cancellation, rollback, and filtered list updates.' + requires: + - 'implement-optimistic-updates-and-cache-writes' + - 'cancel-queries-and-consume-abort-signals' + sources: + - 'https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query' + - 'TanStack/query:docs/framework/react/guides/optimistic-updates.md' + - 'TanStack/query:docs/framework/react/reference/useMutationState.md' + subsystems: + - 'useMutationState' + - 'submittedAt' + - 'queryClient.isMutating' + - 'mutationKey filters' + references: + - 'references/concurrent-optimistic-updates.md' + + - name: 'Query data and forms' + slug: 'query-data-and-forms' + type: 'composition' + domain: 'writing-server-state' + path: 'packages/query-intent/skills/compositions/query-data-and-forms/SKILL.md' + package: 'packages/query-intent' + description: 'Integrate Query data and mutations with forms, dirty client state, background updates, submit prevention, invalidation, and reset-after-save.' + requires: + - 'write-mutations-and-invalidate-related-queries' + - 'selectors-and-derived-state' + sources: + - 'https://tkdodo.eu/blog/react-query-and-forms' + - 'https://tkdodo.eu/blog/deriving-client-state-from-server-state' + - 'TanStack/query:docs/framework/react/guides/mutations.md' + - 'TanStack/query:docs/framework/react/guides/invalidations-from-mutations.md' + subsystems: + - 'form initial values' + - 'dirty fields' + - 'background updates' + - 'mutation submission' + references: + - 'references/query-data-and-forms.md' + + - name: 'Cancel queries and consume AbortSignals' + slug: 'cancel-queries-and-consume-abort-signals' + type: 'core' + domain: 'writing-server-state' + path: 'packages/query-intent/skills/core/cancel-queries-and-consume-abort-signals/SKILL.md' + package: 'packages/query-intent' + description: 'Use AbortSignal-aware query functions, cancelQueries, CancelledError, and rollback behavior without assuming unmount cancels fetches.' + requires: + - 'fetch-and-observe-queries' + sources: + - 'TanStack/query:docs/framework/react/guides/query-cancellation.md' + - 'TanStack/query:docs/reference/QueryClient.md' + - 'TanStack/query:packages/query-core/src/retryer.ts' + references: + - 'references/cancellation-and-abort.md' + + - name: 'Seed, placeholder, select, and transform data' + slug: 'seed-placeholder-select-and-transform-data' + type: 'core' + domain: 'shaping-cache-and-render-behavior' + path: 'packages/query-intent/skills/core/seed-placeholder-select-and-transform-data/SKILL.md' + package: 'packages/query-intent' + description: 'Choose initialData, placeholderData, select, and transformation points without corrupting cache semantics or hiding errors.' + requires: + - 'design-query-keys-and-options' + - 'fetch-and-observe-queries' + sources: + - 'TanStack/query:docs/framework/react/guides/initial-query-data.md' + - 'TanStack/query:docs/framework/react/guides/placeholder-query-data.md' + - 'TanStack/query:docs/framework/react/guides/render-optimizations.md' + - 'TanStack/query:docs/framework/react/reference/queryOptions.md' + references: + - 'references/initial-placeholder-select.md' + - 'references/data-transformation-choices.md' + + - name: 'Selectors and derived state' + slug: 'selectors-and-derived-state' + type: 'core' + domain: 'shaping-cache-and-render-behavior' + path: 'packages/query-intent/skills/core/selectors-and-derived-state/SKILL.md' + package: 'packages/query-intent' + description: 'Use select, stable selector identities, structural sharing, usage-site queryOptions composition, and render-time derivation instead of effect-based state syncing.' + requires: + - 'seed-placeholder-select-and-transform-data' + - 'shape-data-and-render-efficiently' + sources: + - 'https://tkdodo.eu/blog/react-query-selectors-supercharged' + - 'https://tkdodo.eu/blog/deriving-client-state-from-server-state' + - 'TanStack/query:docs/framework/react/guides/render-optimizations.md' + - 'TanStack/query:docs/framework/react/reference/queryOptions.md' + subsystems: + - 'select' + - 'selector identity' + - 'structural sharing' + - 'derived client state' + references: + - 'references/selectors-and-derived-state.md' + + - name: 'Shape data and render efficiently' + slug: 'shape-data-and-render-efficiently' + type: 'framework' + domain: 'shaping-cache-and-render-behavior' + path: 'packages/query-intent/skills/framework/shape-data-and-render-efficiently/SKILL.md' + package: 'packages/query-intent' + description: 'Preserve structural sharing, tracked properties, stable hook deps, and adapter-specific immutability so Query changes do not cause noisy renders.' + requires: + - 'fetch-and-observe-queries' + - 'seed-placeholder-select-and-transform-data' + sources: + - 'TanStack/query:docs/framework/react/guides/render-optimizations.md' + - 'TanStack/query:docs/eslint/no-rest-destructuring.md' + - 'TanStack/query:docs/eslint/no-unstable-deps.md' + - 'TanStack/query:docs/framework/vue/reactivity.md' + subsystems: + - 'React tracked properties' + - 'Preact tracked properties' + - 'Vue immutable results' + references: + - 'references/render-optimization-by-adapter.md' + + - name: 'Prefetch and remove request waterfalls' + slug: 'prefetch-and-remove-request-waterfalls' + type: 'lifecycle' + domain: 'rendering-across-environments' + path: 'packages/query-intent/skills/lifecycle/prefetch-and-remove-request-waterfalls/SKILL.md' + package: 'packages/query-intent' + description: 'Flatten request waterfalls with prefetchQuery, prefetchInfiniteQuery, ensureQueryData, router loaders, and render-before-suspense prefetching.' + requires: + - 'design-query-keys-and-options' + - 'fetch-and-observe-queries' + - 'tune-defaults-freshness-retries-and-refetching' + sources: + - 'TanStack/query:docs/framework/react/guides/prefetching.md' + - 'TanStack/query:docs/framework/react/guides/request-waterfalls.md' + - 'TanStack/query:docs/framework/react/reference/usePrefetchQuery.md' + - 'TanStack/query:docs/framework/react/reference/usePrefetchInfiniteQuery.md' + - 'TanStack/query:docs/reference/QueryClient.md' + - 'TanStack/query:examples/react/react-router/src/routes/root.tsx' + - 'TanStack/query:examples/react/react-router/src/routes/contact.tsx' + subsystems: + - 'event prefetch' + - 'route prefetch' + - 'render prefetch' + - 'query function prefetch' + references: + - 'references/prefetching-and-waterfalls.md' + - 'references/router-loader-prefetch.md' + + - name: 'Compose TanStack Query with TanStack Router and Start' + slug: 'compose-query-with-tanstack-router-and-start' + type: 'composition' + domain: 'rendering-across-environments' + path: 'packages/query-intent/skills/compositions/compose-query-with-tanstack-router-and-start/SKILL.md' + package: 'packages/query-intent' + description: 'Use Query with TanStack Router loaders, router context, @tanstack/react-router-ssr-query, Start routes, SSR hydration, streaming, and redirects.' + requires: + - 'setup-query-client-and-providers' + - 'design-query-keys-and-options' + - 'prefetch-and-remove-request-waterfalls' + sources: + - 'https://tkdodo.eu/blog/tan-stack-router-and-query' + - 'TanStack/router:https://tanstack.com/router/latest/docs/integrations/query' + - 'TanStack/router:https://tanstack.com/router/latest/docs/how-to/setup-ssr' + - 'TanStack/start:https://tanstack.com/start/latest/docs/framework/react/overview' + - 'TanStack/query:docs/framework/react/guides/prefetching.md' + - 'TanStack/query:docs/framework/react/guides/ssr.md' + subsystems: + - 'TanStack Router' + - 'TanStack Start' + - '@tanstack/react-router-ssr-query' + - 'route loaders' + - 'server functions' + references: + - 'references/tanstack-router-start-query.md' + - 'references/router-ssr-query-integration.md' + + - name: 'SSR, hydration, and streaming' + slug: 'ssr-hydration-and-streaming' + type: 'lifecycle' + domain: 'rendering-across-environments' + path: 'packages/query-intent/skills/lifecycle/ssr-hydration-and-streaming/SKILL.md' + package: 'packages/query-intent' + description: 'Use per-request clients, dehydrate/hydrate, HydrationBoundary, TanStack Start, Router SSR Query, Next.js, Nuxt, SvelteKit, SolidStart, Lit SSR, and streamed hydration safely.' + requires: + - 'setup-query-client-and-providers' + - 'prefetch-and-remove-request-waterfalls' + sources: + - 'TanStack/query:docs/framework/react/guides/ssr.md' + - 'TanStack/query:docs/framework/react/guides/advanced-ssr.md' + - 'TanStack/query:docs/framework/react/reference/hydration.md' + - 'TanStack/query:docs/reference/environmentManager.md' + - 'TanStack/query:docs/reference/streamedQuery.md' + - 'TanStack/query:docs/framework/vue/guides/ssr.md' + - 'TanStack/query:docs/framework/svelte/ssr.md' + - 'TanStack/query:docs/framework/solid/guides/ssr.md' + - 'TanStack/query:docs/framework/lit/guides/ssr.md' + subsystems: + - 'TanStack Start' + - 'TanStack Router SSR Query' + - 'Next.js pages router' + - 'Next.js app router' + - 'Nuxt' + - 'SvelteKit' + - 'SolidStart' + - 'Lit SSR' + references: + - 'references/ssr-recipes-by-framework.md' + - 'references/streaming-and-experimental-apis.md' + + - name: 'Use Suspense and error boundaries' + slug: 'use-suspense-and-error-boundaries' + type: 'framework' + domain: 'rendering-across-environments' + path: 'packages/query-intent/skills/framework/use-suspense-and-error-boundaries/SKILL.md' + package: 'packages/query-intent' + description: 'Use suspense query hooks, QueryErrorResetBoundary, throwOnError, React.use query promises, and streamed hydration with the right framework constraints.' + requires: + - 'fetch-and-observe-queries' + - 'prefetch-and-remove-request-waterfalls' + sources: + - 'TanStack/query:docs/framework/react/guides/suspense.md' + - 'TanStack/query:docs/framework/react/reference/QueryErrorResetBoundary.md' + - 'TanStack/query:docs/framework/react/reference/useSuspenseQuery.md' + - 'TanStack/query:docs/framework/react/reference/useSuspenseQueries.md' + - 'TanStack/query:docs/framework/react/reference/useSuspenseInfiniteQuery.md' + - 'TanStack/query:docs/framework/solid/guides/suspense.md' + - 'TanStack/query:docs/framework/vue/guides/suspense.md' + subsystems: + - 'React suspense' + - 'Solid suspense' + - 'Vue suspense' + - 'streamed hydration' + references: + - 'references/suspense-by-framework.md' + + - name: 'Persist offline and restore caches' + slug: 'persist-offline-and-restore-caches' + type: 'composition' + domain: 'persisting-and-synchronizing-caches' + path: 'packages/query-intent/skills/compositions/persist-offline-and-restore-caches/SKILL.md' + package: 'packages/query-intent' + description: 'Persist Query and mutation caches through sync, async, and fine-grained persisters while avoiding restore races, gcTime loss, and offline mutation failures.' + requires: + - 'setup-query-client-and-providers' + - 'tune-defaults-freshness-retries-and-refetching' + - 'write-mutations-and-invalidate-related-queries' + sources: + - 'TanStack/query:docs/framework/react/plugins/persistQueryClient.md' + - 'TanStack/query:docs/framework/react/plugins/createSyncStoragePersister.md' + - 'TanStack/query:docs/framework/react/plugins/createAsyncStoragePersister.md' + - 'TanStack/query:docs/framework/react/plugins/createPersister.md' + - 'TanStack/query:docs/framework/react/guides/network-mode.md' + subsystems: + - 'persistQueryClient' + - 'sync storage persister' + - 'async storage persister' + - 'fine-grained persister' + - 'offline mutations' + references: + - 'references/persistence-recipes.md' + - 'references/offline-mutations.md' + + - name: 'Broadcast, realtime, and multi-tab synchronization' + slug: 'broadcast-realtime-and-multi-tab-synchronization' + type: 'composition' + domain: 'persisting-and-synchronizing-caches' + path: 'packages/query-intent/skills/compositions/broadcast-realtime-and-multi-tab-synchronization/SKILL.md' + package: 'packages/query-intent' + description: 'Synchronize Query caches across tabs and realtime transports with broadcastQueryClient, targeted invalidation, and safe server/browser boundaries.' + requires: + - 'setup-query-client-and-providers' + - 'write-mutations-and-invalidate-related-queries' + - 'persist-offline-and-restore-caches' + sources: + - 'TanStack/query:docs/framework/react/plugins/broadcastQueryClient.md' + - 'TanStack/query:docs/framework/react/guides/query-invalidation.md' + - 'TanStack/query:examples/react/chat/package.json' + subsystems: + - 'broadcastQueryClient' + - 'BroadcastChannel' + - 'WebSocket invalidation' + - 'multi-tab cache sync' + references: + - 'references/broadcast-and-realtime.md' + + - name: 'Use framework adapter reactivity' + slug: 'use-framework-adapter-reactivity' + type: 'framework' + domain: 'framework-adapter-idioms' + path: 'packages/query-intent/skills/framework/use-framework-adapter-reactivity/SKILL.md' + package: 'packages/query-intent' + description: "Apply Query through each adapter's reactivity model, including Vue refs/getters, Solid signals, Svelte stores and runes, Angular signals, Lit controllers, React, and Preact hooks." + requires: + - 'setup-query-client-and-providers' + - 'design-query-keys-and-options' + sources: + - 'TanStack/query:docs/framework/vue/reactivity.md' + - 'TanStack/query:docs/framework/vue/guides/query-options.md' + - 'TanStack/query:docs/framework/solid/guides/query-options.md' + - 'TanStack/query:docs/framework/svelte/overview.md' + - 'TanStack/query:docs/framework/svelte/migrate-from-v5-to-v6.md' + - 'TanStack/query:docs/framework/angular/overview.md' + - 'TanStack/query:docs/framework/angular/zoneless.md' + - 'TanStack/query:docs/framework/lit/guides/reactive-controllers-vs-hooks.md' + subsystems: + - 'React' + - 'Preact' + - 'Vue' + - 'Solid' + - 'Svelte' + - 'Angular' + - 'Lit' + references: + - 'references/adapter-reactivity.md' + - 'references/framework-api-mapping.md' + + - name: 'Debug with devtools' + slug: 'debug-with-devtools' + type: 'framework' + domain: 'operational-quality' + path: 'packages/query-intent/skills/framework/debug-with-devtools/SKILL.md' + package: 'packages/query-intent' + description: 'Install and load Query devtools per adapter, embed panels when needed, avoid production bundles, and understand what devtools can and cannot simulate.' + requires: + - 'setup-query-client-and-providers' + sources: + - 'TanStack/query:docs/framework/react/devtools.md' + - 'TanStack/query:docs/framework/preact/devtools.md' + - 'TanStack/query:docs/framework/vue/devtools.md' + - 'TanStack/query:docs/framework/solid/devtools.md' + - 'TanStack/query:docs/framework/svelte/devtools.md' + - 'TanStack/query:docs/framework/angular/devtools.md' + subsystems: + - 'React Devtools' + - 'Preact Devtools' + - 'Vue Devtools' + - 'Solid Devtools' + - 'Svelte Devtools' + - 'Angular Devtools' + references: + - 'references/devtools-by-adapter.md' + + - name: 'Test query code' + slug: 'test-query-code' + type: 'lifecycle' + domain: 'operational-quality' + path: 'packages/query-intent/skills/lifecycle/test-query-code/SKILL.md' + package: 'packages/query-intent' + description: 'Test Query code with isolated clients, disabled retries, async assertions, cache cleanup, and framework-specific render harnesses.' + requires: + - 'setup-query-client-and-providers' + - 'tune-defaults-freshness-retries-and-refetching' + sources: + - 'TanStack/query:docs/framework/react/guides/testing.md' + - 'TanStack/query:docs/framework/solid/guides/testing.md' + - 'TanStack/query:docs/framework/vue/guides/testing.md' + - 'TanStack/query:docs/framework/angular/guides/testing.md' + - 'TanStack/query:packages/query-core/src/__tests__/queryClient.test.tsx' + - 'TanStack/query:packages/react-query/src/__tests__/useQuery.test.tsx' + references: + - 'references/testing-query.md' + + - name: 'Migrate major versions and codemods' + slug: 'migrate-major-versions-and-codemods' + type: 'lifecycle' + domain: 'operational-quality' + path: 'packages/query-intent/skills/lifecycle/migrate-major-versions-and-codemods/SKILL.md' + package: 'packages/query-intent' + description: 'Migrate Query code across v3, v4, v5, Svelte Query v6, and codemod constraints without reintroducing removed overloads or renamed options.' + sources: + - 'TanStack/query:docs/framework/react/guides/migrating-to-react-query-3.md' + - 'TanStack/query:docs/framework/react/guides/migrating-to-react-query-4.md' + - 'TanStack/query:docs/framework/react/guides/migrating-to-v5.md' + - 'TanStack/query:docs/framework/vue/guides/migrating-to-v5.md' + - 'TanStack/query:docs/framework/svelte/migrate-from-v5-to-v6.md' + - 'TanStack/query:packages/query-codemods/package.json' + subsystems: + - 'v3 migration' + - 'v4 migration' + - 'v5 migration' + - 'Svelte v6 migration' + - 'codemods' + references: + - 'references/migration-v3-v4-v5.md' + - 'references/svelte-v6-migration.md' + + - name: 'Enforce Query best practices with ESLint' + slug: 'enforce-query-best-practices-with-eslint' + type: 'composition' + domain: 'operational-quality' + path: 'packages/query-intent/skills/compositions/enforce-query-best-practices-with-eslint/SKILL.md' + package: 'packages/query-intent' + description: 'Configure @tanstack/eslint-plugin-query recommended or strict rules and use rule-specific fixes for Query keys, options, deps, and infinite or mutation option ordering.' + requires: + - 'design-query-keys-and-options' + - 'fetch-and-observe-queries' + - 'shape-data-and-render-efficiently' + sources: + - 'TanStack/query:docs/eslint/eslint-plugin-query.md' + - 'TanStack/query:docs/eslint/exhaustive-deps.md' + - 'TanStack/query:docs/eslint/no-rest-destructuring.md' + - 'TanStack/query:docs/eslint/no-unstable-deps.md' + - 'TanStack/query:docs/eslint/no-void-query-fn.md' + - 'TanStack/query:docs/eslint/prefer-query-options.md' + - 'TanStack/query:docs/eslint/stable-query-client.md' + - 'TanStack/query:docs/eslint/infinite-query-property-order.md' + - 'TanStack/query:docs/eslint/mutation-property-order.md' + subsystems: + - 'recommended' + - 'recommended-strict' + - 'flat config' + - 'rule fixers' + references: + - 'references/eslint-rules.md' diff --git a/_artifacts/tkdodo_cross_check.md b/_artifacts/tkdodo_cross_check.md new file mode 100644 index 00000000000..f43c76f25c3 --- /dev/null +++ b/_artifacts/tkdodo_cross_check.md @@ -0,0 +1,62 @@ +# TKDodo Cross-Check + +Checked on 2026-06-03 against `https://tkdodo.eu/blog/tan-stack-router-and-query`, which lists 33 React Query series parts, and `https://tkdodo.eu/blog/all` for adjacent Query-tagged posts. + +## Coverage Decisions + +| Priority | Post | Coverage | +| -------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| #1 | Practical React Query | `fetch-and-observe-queries`, `tune-defaults-freshness-retries-and-refetching` | +| #2 | React Query Data Transformations | `seed-placeholder-select-and-transform-data`, `selectors-and-derived-state` | +| #3 | React Query Render Optimizations | `shape-data-and-render-efficiently`, `selectors-and-derived-state` | +| #4 | Status Checks in React Query | Added `handle-status-and-errors` | +| #5 | Testing React Query | `test-query-code` | +| #6 | React Query and TypeScript | `design-query-keys-and-options`, `build-query-abstractions` | +| #7 | Using WebSockets with React Query | `broadcast-realtime-and-multi-tab-synchronization` | +| #8 | Effective React Query Keys | `design-query-keys-and-options` | +| #8a | Leveraging the Query Function Context | `design-query-keys-and-options`, `cancel-queries-and-consume-abort-signals` | +| #9 | Placeholder and Initial Data in React Query | `seed-placeholder-select-and-transform-data` | +| #10 | React Query as a State Manager | `selectors-and-derived-state`, `query-data-and-forms` | +| #11 | React Query Error Handling | Added `handle-status-and-errors` | +| #12 | Mastering Mutations in React Query | `write-mutations-and-invalidate-related-queries` | +| #13 | Offline React Query | `persist-offline-and-restore-caches` | +| #14 | React Query and Forms | Added `query-data-and-forms` | +| #15 | React Query FAQs | Distributed across keys, mutations, defaults, forms, and status skills | +| #16 | React Query meets React Router | `prefetch-and-remove-request-waterfalls`; React Router remains secondary to TanStack Router/Start | +| #17 | Seeding the Query Cache | `seed-placeholder-select-and-transform-data` | +| #18 | Inside React Query | Added `understand-query-internals-and-observers` | +| #19 | Type-safe React Query | `design-query-keys-and-options`, `build-query-abstractions` | +| #20 | You Might Not Need React Query | `ssr-hydration-and-streaming`; no separate skill yet because it is decision guidance | +| #21 | Thinking in React Query | Distributed mental-model coverage, especially `understand-query-internals-and-observers` | +| #22 | React Query and React Context | `setup-query-client-and-providers`, `use-framework-adapter-reactivity` | +| #23 | Why You Want React Query | Introductory rationale, covered by higher-signal operational skills | +| #24 | The Query Options API | `design-query-keys-and-options`, added `build-query-abstractions` | +| #25 | Automatic Query Invalidation after Mutations | Added `automatic-invalidation-after-mutations` | +| #26 | How Infinite Queries work | `paginate-and-build-infinite-queries` | +| #27 | React Query API Design - Lessons Learned | `build-query-abstractions`, `migrate-major-versions-and-codemods` | +| #28 | React Query - The Bad Parts | Cross-cutting tradeoffs in `query-data-and-forms`, `handle-status-and-errors`, `ssr-hydration-and-streaming` | +| #29 | Concurrent Optimistic Updates in React Query | Added `concurrent-optimistic-updates` | +| #30 | React Query Selectors, Supercharged | Added `selectors-and-derived-state` | +| #31 | Creating Query Abstractions | Added `build-query-abstractions` | +| #32 | TanStack Router and Query | Tightened `compose-query-with-tanstack-router-and-start` | + +## Adjacent Posts Promoted + +| Post | Reason | Coverage | +| ----------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------ | +| Deriving Client State from Server State | Directly informs forms, selectors, and state-sync avoidance | `selectors-and-derived-state`, `query-data-and-forms` | +| React 19 and Suspense - A Drama in 3 Acts | Relevant to suspense and streaming but React-version specific | `use-suspense-and-error-boundaries`, `ssr-hydration-and-streaming` | + +## Skills Added From This Audit + +- `framework/handle-status-and-errors` +- `compositions/query-data-and-forms` +- `compositions/automatic-invalidation-after-mutations` +- `core/concurrent-optimistic-updates` +- `core/selectors-and-derived-state` +- `core/build-query-abstractions` +- `core/understand-query-internals-and-observers` + +## Existing Skill Tightened + +- `compositions/compose-query-with-tanstack-router-and-start`: added TKDodo #32 source and guidance to use one QueryClient in router/provider, set `defaultPreloadStaleTime: 0`, treat loaders as cache priming, and read route data through Query observers instead of `useLoaderData`. diff --git a/package.json b/package.json index cf09bf7526c..ca0e4e5f558 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,8 @@ "@eslint-react/eslint-plugin": "^2.0.1", "@size-limit/preset-small-lib": "^12.0.0", "@tanstack/eslint-config": "0.3.2", + "@tanstack/intent": "^0.0.41", + "@tanstack/query-intent": "workspace:*", "@tanstack/typedoc-config": "0.3.1", "@tanstack/vite-config": "0.4.3", "@testing-library/jest-dom": "^6.8.0", diff --git a/packages/query-intent/package.json b/packages/query-intent/package.json new file mode 100644 index 00000000000..0a0f2ad428a --- /dev/null +++ b/packages/query-intent/package.json @@ -0,0 +1,29 @@ +{ + "name": "@tanstack/query-intent", + "version": "5.101.0", + "description": "Intent skills for TanStack Query, framework adapters, and ecosystem integrations", + "author": "tannerlinsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/query.git", + "directory": "packages/query-intent" + }, + "homepage": "https://tanstack.com/query", + "type": "module", + "keywords": [ + "tanstack-intent" + ], + "files": [ + "skills", + "!skills/_artifacts" + ], + "intent": { + "version": 1, + "repo": "TanStack/query", + "docs": "https://tanstack.com/query" + }, + "devDependencies": { + "@tanstack/intent": "^0.0.41" + } +} diff --git a/packages/query-intent/skills/compositions/automatic-invalidation-after-mutations/SKILL.md b/packages/query-intent/skills/compositions/automatic-invalidation-after-mutations/SKILL.md new file mode 100644 index 00000000000..e4af9a7f462 --- /dev/null +++ b/packages/query-intent/skills/compositions/automatic-invalidation-after-mutations/SKILL.md @@ -0,0 +1,131 @@ +--- +name: compositions/automatic-invalidation-after-mutations +description: > + Use this when designing automatic invalidation policies for TanStack Query + mutations with MutationCache callbacks, mutationKey-to-queryKey matching, + mutation meta invalidation tags, awaited invalidation, and exclusions for + static or unrelated queries. +type: composition +library: TanStack Query +library_version: '5.101.0' +requires: + - core/write-mutations-and-invalidate-related-queries + - core/design-query-keys-and-options +sources: + - https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations + - TanStack/query:docs/framework/react/guides/invalidations-from-mutations.md + - TanStack/query:docs/reference/MutationCache.md + - TanStack/query:docs/reference/QueryClient.md +--- + +## Core Patterns + +Use local mutation callbacks for one-off behavior. Use `MutationCache` callbacks when the app wants a consistent invalidation policy for every mutation. + +### Global invalidation after successful mutations + +```ts +import { MutationCache, QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + mutationCache: new MutationCache({ + onSuccess: (_data, _variables, _context, mutation) => { + return queryClient.invalidateQueries({ + queryKey: mutation.options.mutationKey, + }) + }, + }), +}) +``` + +If a mutation has `mutationKey: ['issues']`, this invalidates matching issue queries. If it has no mutation key, this becomes a broad invalidation policy, so only use that deliberately. + +### Use meta for explicit invalidation tags + +```ts +import { matchQuery, MutationCache, QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient({ + mutationCache: new MutationCache({ + onSuccess: (_data, _variables, _context, mutation) => { + return queryClient.invalidateQueries({ + predicate: (query) => + mutation.meta?.invalidates?.some((queryKey) => + matchQuery({ queryKey }, query), + ) ?? true, + }) + }, + }), +}) +``` + +## Common Mistakes + +### HIGH Invalidating the whole app for every mutation + +Wrong: + +```ts +new MutationCache({ + onSuccess: () => queryClient.invalidateQueries(), +}) +``` + +Correct: + +```ts +new MutationCache({ + onSuccess: (_data, _variables, _context, mutation) => + queryClient.invalidateQueries({ queryKey: mutation.options.mutationKey }), +}) +``` + +Global policies need scope. Reach for mutation keys or meta tags before invalidating everything. + +Source: https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations + +### HIGH Not returning invalidation when pending UI depends on refetch + +Wrong: + +```ts +onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['todos'] }) +} +``` + +Correct: + +```ts +onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }) +``` + +Returning the promise keeps the mutation pending until the invalidation refetch completes. + +Source: TanStack/query:docs/framework/react/guides/invalidations-from-mutations.md + +### MEDIUM Refetching data that should be static + +Wrong: + +```ts +useQuery({ + queryKey: ['build-info'], + queryFn: fetchBuildInfo, + staleTime: Infinity, +}) +``` + +Correct: + +```ts +useQuery({ + queryKey: ['build-info'], + queryFn: fetchBuildInfo, + staleTime: 'static', +}) +``` + +If a query must not refetch even after broad manual invalidation, mark it with `staleTime: 'static'`. + +Source: https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations diff --git a/packages/query-intent/skills/compositions/broadcast-realtime-and-multi-tab-synchronization/SKILL.md b/packages/query-intent/skills/compositions/broadcast-realtime-and-multi-tab-synchronization/SKILL.md new file mode 100644 index 00000000000..6f71e4b2e5e --- /dev/null +++ b/packages/query-intent/skills/compositions/broadcast-realtime-and-multi-tab-synchronization/SKILL.md @@ -0,0 +1,150 @@ +--- +name: compositions/broadcast-realtime-and-multi-tab-synchronization +description: > + Use this when synchronizing TanStack Query caches with broadcastQueryClient, + BroadcastChannel, multi-tab cache sync, realtime invalidation, WebSocket + events, server/browser boundaries, and experimental broadcast behavior. +type: composition +library: TanStack Query +library_version: '5.101.0' +requires: + - lifecycle/setup-query-client-and-providers + - core/write-mutations-and-invalidate-related-queries + - compositions/persist-offline-and-restore-caches +sources: + - TanStack/query:docs/framework/react/plugins/broadcastQueryClient.md + - TanStack/query:docs/framework/react/guides/query-invalidation.md + - TanStack/query:examples/react/chat/package.json +--- + +## Setup + +```ts +import { QueryClient } from '@tanstack/react-query' +import { broadcastQueryClient } from '@tanstack/query-broadcast-client-experimental' + +export const queryClient = new QueryClient() + +if (typeof window !== 'undefined') { + broadcastQueryClient({ queryClient, broadcastChannel: 'app-query-cache' }) +} +``` + +## Core Integration Patterns + +### Invalidate from realtime events + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +export function onTodoChanged(todoId: number) { + return queryClient.invalidateQueries({ queryKey: ['todo', todoId] }) +} +``` + +### Update narrow cache slices + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +export function onTodoTitle(todo: { id: number; title: string }) { + queryClient.setQueryData(['todo', todo.id], todo) +} +``` + +### Keep broadcast client-side + +```ts +import { QueryClient } from '@tanstack/react-query' +import { broadcastQueryClient } from '@tanstack/query-broadcast-client-experimental' + +export function installBroadcast(queryClient: QueryClient) { + if (typeof window === 'undefined') return + broadcastQueryClient({ queryClient, broadcastChannel: 'query-cache' }) +} +``` + +## Common Mistakes + +### MEDIUM Unlocked experimental broadcast + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' +import { broadcastQueryClient } from '@tanstack/query-broadcast-client-experimental' + +broadcastQueryClient({ queryClient: new QueryClient() }) +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' +import { broadcastQueryClient } from '@tanstack/query-broadcast-client-experimental' + +if (typeof window !== 'undefined') { + broadcastQueryClient({ + queryClient: new QueryClient(), + broadcastChannel: 'query-cache-v1', + }) +} +``` + +The broadcast package is experimental and should be deliberately isolated behind browser-only setup. + +Source: TanStack/query:docs/framework/react/plugins/broadcastQueryClient.md + +### MEDIUM Over-normalized realtime writes + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() +queryClient.setQueryData(['todos'], { byId: { 1: { id: 1 } }, allIds: [1] }) +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() +queryClient.invalidateQueries({ queryKey: ['todos'] }) +``` + +Query is not a normalized cache; invalidation is often safer than duplicating server state models. + +Source: TanStack/query:docs/framework/react/guides/query-invalidation.md + +### HIGH Broadcast on server + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' +import { broadcastQueryClient } from '@tanstack/query-broadcast-client-experimental' + +export const queryClient = new QueryClient() +broadcastQueryClient({ queryClient }) +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' +import { broadcastQueryClient } from '@tanstack/query-broadcast-client-experimental' + +export const queryClient = new QueryClient() +if (typeof window !== 'undefined') broadcastQueryClient({ queryClient }) +``` + +BroadcastChannel is a browser primitive; server setup should not install it. + +Source: TanStack/query:docs/framework/react/plugins/broadcastQueryClient.md diff --git a/packages/query-intent/skills/compositions/compose-query-with-tanstack-router-and-start/SKILL.md b/packages/query-intent/skills/compositions/compose-query-with-tanstack-router-and-start/SKILL.md new file mode 100644 index 00000000000..45dae538c93 --- /dev/null +++ b/packages/query-intent/skills/compositions/compose-query-with-tanstack-router-and-start/SKILL.md @@ -0,0 +1,260 @@ +--- +name: compositions/compose-query-with-tanstack-router-and-start +description: > + Use this when combining TanStack Query with TanStack Router or TanStack Start: + router context QueryClient, createFileRoute loaders, ensureQueryData, + @tanstack/react-router-ssr-query, setupRouterSsrQueryIntegration, SSR hydration, + streaming, redirects, and Start server functions. +type: composition +library: TanStack Query +library_version: '5.101.0' +requires: + - lifecycle/setup-query-client-and-providers + - core/design-query-keys-and-options + - lifecycle/prefetch-and-remove-request-waterfalls +sources: + - https://tkdodo.eu/blog/tan-stack-router-and-query + - TanStack/router:https://tanstack.com/router/latest/docs/integrations/query + - TanStack/router:https://tanstack.com/router/latest/docs/how-to/setup-ssr + - TanStack/start:https://tanstack.com/start/latest/docs/framework/react/overview + - TanStack/query:docs/framework/react/guides/prefetching.md + - TanStack/query:docs/framework/react/guides/ssr.md +--- + +## Setup + +```tsx +import { + QueryClient, + QueryClientProvider, + useSuspenseQuery, +} from '@tanstack/react-query' +import { + createFileRoute, + createRouter, + RouterProvider, +} from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +const queryClient = new QueryClient() + +const postsQuery = { + queryKey: ['posts'], + queryFn: async () => [{ id: 1, title: 'Router first' }], +} + +export const Route = createFileRoute('/posts')({ + loader: ({ context }) => context.queryClient.ensureQueryData(postsQuery), + component: PostsPage, +}) + +function PostsPage() { + const { data } = useSuspenseQuery(postsQuery) + return
    {JSON.stringify(data)}
    +} + +const router = createRouter({ routeTree, context: { queryClient } }) + +export function App() { + return ( + + + + ) +} +``` + +## Core Integration Patterns + +### Put QueryClient in router context + +```ts +import { QueryClient } from '@tanstack/react-query' +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +const queryClient = new QueryClient() + +export const router = createRouter({ + routeTree, + context: { queryClient }, + defaultPreloadStaleTime: 0, +}) +``` + +Set `defaultPreloadStaleTime: 0` when Query owns server-state freshness. Router still preloads and runs loaders, but Query is the cache authority. + +### Use loader ensureQueryData for route data + +```ts +import { createFileRoute } from '@tanstack/react-router' + +const todoQuery = (todoId: string) => ({ + queryKey: ['todo', todoId], + queryFn: async () => ({ id: todoId, title: 'Loaded' }), +}) + +export const Route = createFileRoute('/todos/$todoId')({ + loader: ({ context, params }) => + context.queryClient.ensureQueryData(todoQuery(params.todoId)), +}) +``` + +Treat loaders as event handlers that prime the Query cache. Components should still call `useQuery` or `useSuspenseQuery` so the route has an active Query observer. + +### Prefer Router SSR Query integration for Router SSR + +```ts +import { QueryClient } from '@tanstack/react-query' +import { createRouter } from '@tanstack/react-router' +import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query' +import { routeTree } from './routeTree.gen' + +const queryClient = new QueryClient() +const router = createRouter({ routeTree, context: { queryClient } }) + +setupRouterSsrQueryIntegration({ router, queryClient }) +``` + +## Common Mistakes + +### CRITICAL Component-only Query creates route waterfall + +Wrong: + +```tsx +import { createFileRoute } from '@tanstack/react-router' +import { useSuspenseQuery } from '@tanstack/react-query' + +export const Route = createFileRoute('/posts')({ component: PostsPage }) +function PostsPage() { + const { data } = useSuspenseQuery({ + queryKey: ['posts'], + queryFn: async () => [{ id: 1 }], + }) + return
    {JSON.stringify(data)}
    +} +``` + +Correct: + +```tsx +import { createFileRoute } from '@tanstack/react-router' +import { useSuspenseQuery } from '@tanstack/react-query' + +const postsQuery = { queryKey: ['posts'], queryFn: async () => [{ id: 1 }] } +export const Route = createFileRoute('/posts')({ + loader: ({ context }) => context.queryClient.ensureQueryData(postsQuery), + component: PostsPage, +}) +function PostsPage() { + const { data } = useSuspenseQuery(postsQuery) + return
    {JSON.stringify(data)}
    +} +``` + +The router can load route-critical data before component render. + +Source: TanStack/router:https://tanstack.com/router/latest/docs/integrations/query + +### CRITICAL Hand-rolled Router hydration + +Wrong: + +```tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { createRouter, RouterProvider } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +const queryClient = new QueryClient() +const router = createRouter({ routeTree }) +export function App() { + return ( + + + + ) +} +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' +import { createRouter } from '@tanstack/react-router' +import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query' +import { routeTree } from './routeTree.gen' + +const queryClient = new QueryClient() +const router = createRouter({ routeTree, context: { queryClient } }) +setupRouterSsrQueryIntegration({ router, queryClient }) +``` + +The Router SSR Query integration handles dehydration, hydration, streamed query results, redirects, and provider wrapping. + +Source: TanStack/router:https://tanstack.com/router/latest/docs/integrations/query + +### HIGH Next.js mental model copied into Start + +Wrong: + +```tsx +export default async function Page() { + const posts = await Promise.resolve([{ id: 1 }]) + return
    {JSON.stringify(posts)}
    +} +``` + +Correct: + +```tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts')({ + loader: ({ context }) => + context.queryClient.ensureQueryData({ + queryKey: ['posts'], + queryFn: async () => [{ id: 1 }], + }), + component: () =>

    Posts loaded by the route

    , +}) +``` + +Start is powered by TanStack Router; use file routes, loaders, server functions, and Router SSR before Next-specific app/pages APIs. + +Source: TanStack/start:https://tanstack.com/start/latest/docs/framework/react/overview + +### HIGH Reading loader data instead of observing Query + +Wrong: + +```tsx +import { createFileRoute, useLoaderData } from '@tanstack/react-router' + +export const Route = createFileRoute('/posts')({ + loader: ({ context }) => context.queryClient.ensureQueryData(postsQuery), + component: PostsPage, +}) + +function PostsPage() { + const posts = useLoaderData({ from: '/posts' }) + return
    {JSON.stringify(posts)}
    +} +``` + +Correct: + +```tsx +import { useSuspenseQuery } from '@tanstack/react-query' + +function PostsPage() { + const { data } = useSuspenseQuery(postsQuery) + return
    {JSON.stringify(data)}
    +} +``` + +The loader primes the cache. The component still needs an active Query observer for refetch triggers, invalidation, and garbage collection semantics. + +Source: https://tkdodo.eu/blog/tan-stack-router-and-query + +See also: `lifecycle/ssr-hydration-and-streaming` for framework SSR recipes. diff --git a/packages/query-intent/skills/compositions/enforce-query-best-practices-with-eslint/SKILL.md b/packages/query-intent/skills/compositions/enforce-query-best-practices-with-eslint/SKILL.md new file mode 100644 index 00000000000..d2644b48343 --- /dev/null +++ b/packages/query-intent/skills/compositions/enforce-query-best-practices-with-eslint/SKILL.md @@ -0,0 +1,167 @@ +--- +name: compositions/enforce-query-best-practices-with-eslint +description: > + Use this when configuring @tanstack/eslint-plugin-query, flat/recommended, + flat/recommended-strict, exhaustive-deps, no-rest-destructuring, + no-unstable-deps, no-void-query-fn, stable-query-client, prefer-query-options, + infinite-query-property-order, and mutation-property-order. +type: composition +library: TanStack Query +library_version: '5.101.0' +requires: + - core/design-query-keys-and-options + - core/fetch-and-observe-queries + - framework/shape-data-and-render-efficiently +sources: + - TanStack/query:docs/eslint/eslint-plugin-query.md + - TanStack/query:docs/eslint/exhaustive-deps.md + - TanStack/query:docs/eslint/no-rest-destructuring.md + - TanStack/query:docs/eslint/no-unstable-deps.md + - TanStack/query:docs/eslint/no-void-query-fn.md + - TanStack/query:docs/eslint/prefer-query-options.md + - TanStack/query:docs/eslint/stable-query-client.md + - TanStack/query:docs/eslint/infinite-query-property-order.md + - TanStack/query:docs/eslint/mutation-property-order.md +--- + +## Setup + +```js +import pluginQuery from '@tanstack/eslint-plugin-query' + +export default [...pluginQuery.configs['flat/recommended']] +``` + +## Core Integration Patterns + +### Use strict when generating Query-heavy code + +```js +import pluginQuery from '@tanstack/eslint-plugin-query' + +export default [...pluginQuery.configs['flat/recommended-strict']] +``` + +### Prefer option factories + +```ts +import { queryOptions, useQuery } from '@tanstack/react-query' + +const todosOptions = queryOptions({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], +}) + +export function useTodos() { + return useQuery(todosOptions) +} +``` + +### Keep inference-sensitive property order + +```ts +import { useInfiniteQuery } from '@tanstack/react-query' + +export function useFeed() { + return useInfiniteQuery({ + queryKey: ['feed'], + queryFn: async ({ pageParam }) => ({ nextCursor: pageParam + 1 }), + initialPageParam: 0, + getNextPageParam: (page) => page.nextCursor, + }) +} +``` + +## Common Mistakes + +### MEDIUM Strict rule not enabled + +Wrong: + +```js +import pluginQuery from '@tanstack/eslint-plugin-query' + +export default [...pluginQuery.configs['flat/recommended']] +``` + +Correct: + +```js +import pluginQuery from '@tanstack/eslint-plugin-query' + +export default [...pluginQuery.configs['flat/recommended-strict']] +``` + +Strict mode catches option-factory and inference patterns that agents commonly miss. + +Source: TanStack/query:docs/eslint/eslint-plugin-query.md + +### MEDIUM Infinite option property order + +Wrong: + +```ts +import { useInfiniteQuery } from '@tanstack/react-query' + +export function useFeed() { + return useInfiniteQuery({ + queryFn: async () => ({ nextCursor: 1 }), + queryKey: ['feed'], + getNextPageParam: (page) => page.nextCursor, + initialPageParam: 0, + }) +} +``` + +Correct: + +```ts +import { useInfiniteQuery } from '@tanstack/react-query' + +export function useFeed() { + return useInfiniteQuery({ + queryKey: ['feed'], + queryFn: async ({ pageParam }) => ({ nextCursor: pageParam + 1 }), + initialPageParam: 0, + getNextPageParam: (page) => page.nextCursor, + }) +} +``` + +Some infinite-query inference depends on stable option ordering. + +Source: TanStack/query:docs/eslint/infinite-query-property-order.md + +### MEDIUM Mutation option property order + +Wrong: + +```ts +import { useMutation } from '@tanstack/react-query' + +export function useSave() { + return useMutation({ + onSuccess: () => console.log('saved'), + mutationFn: async (title: string) => title, + }) +} +``` + +Correct: + +```ts +import { useMutation } from '@tanstack/react-query' + +export function useSave() { + return useMutation({ + mutationFn: async (title: string) => title, + onSuccess: () => console.log('saved'), + }) +} +``` + +The mutation property order rule preserves inference for mutation options. + +Source: TanStack/query:docs/eslint/mutation-property-order.md + +See also: `core/design-query-keys-and-options` for the patterns these rules protect. diff --git a/packages/query-intent/skills/compositions/persist-offline-and-restore-caches/SKILL.md b/packages/query-intent/skills/compositions/persist-offline-and-restore-caches/SKILL.md new file mode 100644 index 00000000000..259e17595fe --- /dev/null +++ b/packages/query-intent/skills/compositions/persist-offline-and-restore-caches/SKILL.md @@ -0,0 +1,191 @@ +--- +name: compositions/persist-offline-and-restore-caches +description: > + Use this when using persistQueryClient, PersistQueryClientProvider, + createSyncStoragePersister, createAsyncStoragePersister, experimental + fine-grained persisters, offline mutations, resumePausedMutations, maxAge, + gcTime, and restore races. +type: composition +library: TanStack Query +library_version: '5.101.0' +requires: + - lifecycle/setup-query-client-and-providers + - core/tune-defaults-freshness-retries-and-refetching + - core/write-mutations-and-invalidate-related-queries +sources: + - TanStack/query:docs/framework/react/plugins/persistQueryClient.md + - TanStack/query:docs/framework/react/plugins/createSyncStoragePersister.md + - TanStack/query:docs/framework/react/plugins/createAsyncStoragePersister.md + - TanStack/query:docs/framework/react/plugins/createPersister.md + - TanStack/query:docs/framework/react/guides/network-mode.md +--- + +## Setup + +```tsx +import * as React from 'react' +import { QueryClient } from '@tanstack/react-query' +import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' +import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' + +const queryClient = new QueryClient({ + defaultOptions: { queries: { gcTime: 24 * 60 * 60 * 1000 } }, +}) + +const persister = createSyncStoragePersister({ + storage: window.localStorage, +}) + +export function AppProviders(props: { children: React.ReactNode }) { + return ( + + {props.children} + + ) +} +``` + +## Core Integration Patterns + +### Resume paused mutations after restore + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient() + +queryClient.setMutationDefaults(['todos'], { + mutationFn: async (todo: { id: number; title: string }) => todo, +}) + +export function resumeMutations() { + return queryClient.resumePausedMutations() +} +``` + +### Align cache lifetime with persistence + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { gcTime: 7 * 24 * 60 * 60 * 1000 }, + }, +}) +``` + +### Use networkMode for offline-first writes + +```ts +import { useMutation } from '@tanstack/react-query' + +export function useSaveDraft() { + return useMutation({ + mutationKey: ['saveDraft'], + mutationFn: async (draft: { body: string }) => draft, + networkMode: 'offlineFirst', + }) +} +``` + +## Common Mistakes + +### HIGH gcTime shorter than maxAge + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + defaultOptions: { queries: { gcTime: 5 * 60 * 1000 } }, +}) +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + defaultOptions: { queries: { gcTime: 24 * 60 * 60 * 1000 } }, +}) +``` + +Persisted data can be garbage-collected before the persister maxAge can restore it. + +Source: TanStack/query:docs/framework/react/plugins/persistQueryClient.md + +### CRITICAL Rendering before restore + +Wrong: + +```tsx +import { QueryClientProvider, QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() +export function App(props: { children: React.ReactNode }) { + return ( + + {props.children} + + ) +} +``` + +Correct: + +```tsx +import { QueryClient } from '@tanstack/react-query' +import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' +import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' + +const queryClient = new QueryClient() +const persister = createSyncStoragePersister({ storage: window.localStorage }) + +export function App(props: { children: React.ReactNode }) { + return ( + + {props.children} + + ) +} +``` + +The persistence provider prevents query fetching while restore is in progress. + +Source: TanStack/query:docs/framework/react/plugins/persistQueryClient.md + +### CRITICAL Missing default mutationFn + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient() +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient() +queryClient.setMutationDefaults(['saveDraft'], { + mutationFn: async (draft: { body: string }) => draft, +}) +``` + +Paused persisted mutations cannot resume without a serializable mutation key mapped to a default mutationFn. + +Source: TanStack/query:docs/framework/react/plugins/persistQueryClient.md + +See also: `core/tune-defaults-freshness-retries-and-refetching` for gcTime and networkMode. diff --git a/packages/query-intent/skills/compositions/query-data-and-forms/SKILL.md b/packages/query-intent/skills/compositions/query-data-and-forms/SKILL.md new file mode 100644 index 00000000000..9d3dbc7b443 --- /dev/null +++ b/packages/query-intent/skills/compositions/query-data-and-forms/SKILL.md @@ -0,0 +1,165 @@ +--- +name: compositions/query-data-and-forms +description: > + Use this when integrating TanStack Query data and mutations with forms: + initial server state, editable client state, dirty fields, background updates, + submit prevention, invalidation, reset-after-save, validation errors, and + avoiding blind copies from Query cache into local form state. +type: composition +library: TanStack Query +library_version: '5.101.0' +requires: + - core/write-mutations-and-invalidate-related-queries + - core/selectors-and-derived-state +sources: + - https://tkdodo.eu/blog/react-query-and-forms + - https://tkdodo.eu/blog/deriving-client-state-from-server-state + - TanStack/query:docs/framework/react/guides/mutations.md + - TanStack/query:docs/framework/react/guides/invalidations-from-mutations.md +--- + +## Core Patterns + +Treat form state as client state after the user starts editing. Server state can initialize the form, but it should not be copied blindly on every query update. + +### Initial data only + +Use this when one user owns the form and background updates are not useful while editing. + +```tsx +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' + +function PersonDetail({ id }: { id: string }) { + const queryClient = useQueryClient() + const person = useQuery({ + queryKey: ['person', id], + queryFn: () => fetchPerson(id), + staleTime: Infinity, + }) + + const updatePerson = useMutation({ + mutationFn: savePerson, + onSuccess: () => + queryClient.invalidateQueries({ queryKey: ['person', id] }), + }) + + if (!person.data) return

    Loading...

    + return ( + + ) +} +``` + +### Derived server plus client state + +Use this when background updates should remain visible for untouched fields. + +```tsx +import * as React from 'react' +import { useQuery } from '@tanstack/react-query' + +function PersonName({ id }: { id: string }) { + const { data } = useQuery({ + queryKey: ['person', id], + queryFn: () => fetchPerson(id), + }) + const [draft, setDraft] = React.useState<{ firstName?: string }>({}) + + if (!data) return

    Loading...

    + + return ( + setDraft({ firstName: event.target.value })} + /> + ) +} +``` + +### Reset after awaited invalidation + +```tsx +const mutation = useMutation({ + mutationFn: savePerson, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['person', id] }), +}) + +function submit(values: PersonFormValues) { + mutation.mutate(values, { onSuccess: () => resetFormDraft() }) +} +``` + +Return the invalidation promise from `onSuccess` or `onSettled` when the saved server state must be back in the cache before the form resets. + +## Common Mistakes + +### HIGH Initializing form defaults before query data exists + +Wrong: + +```tsx +const { data } = useQuery({ + queryKey: ['person', id], + queryFn: () => fetchPerson(id), +}) +const [draft, setDraft] = React.useState(data) +``` + +Correct: + +```tsx +const { data } = useQuery({ + queryKey: ['person', id], + queryFn: () => fetchPerson(id), +}) +if (!data) return

    Loading...

    +return +``` + +The first render usually has no query data. Split the form boundary or derive field values from query data plus draft state. + +Source: https://tkdodo.eu/blog/react-query-and-forms + +### HIGH Background refetch overwrites or hides dirty client state + +Wrong: + +```tsx +React.useEffect(() => { + setDraft(personQuery.data) +}, [personQuery.data]) +``` + +Correct: + +```tsx +const shownFirstName = draft.firstName ?? personQuery.data?.firstName ?? '' +``` + +Do not synchronize every server update into the draft. Derive displayed values or intentionally opt out of background updates for initial-only forms. + +Source: https://tkdodo.eu/blog/deriving-client-state-from-server-state + +### MEDIUM Form submit can run twice + +Wrong: + +```tsx + +``` + +Correct: + +```tsx + +``` + +Use mutation state to block duplicate writes while a submission is in flight. + +Source: https://tkdodo.eu/blog/react-query-and-forms diff --git a/packages/query-intent/skills/core/build-query-abstractions/SKILL.md b/packages/query-intent/skills/core/build-query-abstractions/SKILL.md new file mode 100644 index 00000000000..7dcacb98115 --- /dev/null +++ b/packages/query-intent/skills/core/build-query-abstractions/SKILL.md @@ -0,0 +1,136 @@ +--- +name: core/build-query-abstractions +description: > + Use this when creating TanStack Query abstractions: queryOptions factories, + feature-local key modules, custom hooks built on top of options factories, + TypeScript inference, avoiding wide UseQueryOptions wrappers, and sharing + query configuration across hooks, loaders, prefetches, and QueryClient calls. +type: core +library: TanStack Query +library_version: '5.101.0' +requires: + - core/design-query-keys-and-options +sources: + - https://tkdodo.eu/blog/creating-query-abstractions + - https://tkdodo.eu/blog/the-query-options-api + - TanStack/query:docs/framework/react/guides/query-options.md + - TanStack/query:docs/framework/react/typescript.md +--- + +## Core Patterns + +Prefer a `queryOptions` factory as the base abstraction. Hooks, loaders, prefetches, suspense queries, and `QueryClient` calls can all consume it. + +```ts +import { queryOptions, useQuery, useSuspenseQuery } from '@tanstack/react-query' + +export function invoiceOptions(id: number) { + return queryOptions({ + queryKey: ['invoice', id], + queryFn: () => fetchInvoice(id), + staleTime: 60_000, + }) +} + +export function useInvoice(id: number) { + return useQuery(invoiceOptions(id)) +} + +export function useSuspenseInvoice(id: number) { + return useSuspenseQuery(invoiceOptions(id)) +} +``` + +Compose one-off options at the usage site: + +```ts +const invoice = useQuery({ + ...invoiceOptions(id), + select: (data) => data.createdAt, + throwOnError: true, +}) +``` + +## Common Mistakes + +### HIGH Custom hook is the only abstraction + +Wrong: + +```ts +export function useInvoice(id: number) { + return useQuery({ + queryKey: ['invoice', id], + queryFn: () => fetchInvoice(id), + }) +} +``` + +Correct: + +```ts +export function invoiceOptions(id: number) { + return queryOptions({ + queryKey: ['invoice', id], + queryFn: () => fetchInvoice(id), + }) +} + +export function useInvoice(id: number) { + return useQuery(invoiceOptions(id)) +} +``` + +Custom hooks cannot run in route loaders, server prefetches, or event handlers. Options factories can. + +Source: https://tkdodo.eu/blog/creating-query-abstractions + +### HIGH Wide UseQueryOptions wrapper breaks inference + +Wrong: + +```ts +function useInvoice(id: number, options?: Partial>) { + return useQuery({ + queryKey: ['invoice', id], + queryFn: () => fetchInvoice(id), + ...options, + }) +} +``` + +Correct: + +```ts +useQuery({ + ...invoiceOptions(id), + select: (invoice) => invoice.createdAt, +}) +``` + +Let `queryOptions` and usage-site composition preserve `select` inference. + +Source: https://tkdodo.eu/blog/creating-query-abstractions + +### MEDIUM Wrapper hides Query result state + +Wrong: + +```ts +export function useInvoice(id: number) { + const { data } = useQuery(invoiceOptions(id)) + return data +} +``` + +Correct: + +```ts +export function useInvoice(id: number) { + return useQuery(invoiceOptions(id)) +} +``` + +Keep the Query result surface available unless the abstraction owns every loading, error, and refetch behavior. + +Source: TanStack/query:docs/framework/react/guides/queries.md diff --git a/packages/query-intent/skills/core/cancel-queries-and-consume-abort-signals/SKILL.md b/packages/query-intent/skills/core/cancel-queries-and-consume-abort-signals/SKILL.md new file mode 100644 index 00000000000..43142accd23 --- /dev/null +++ b/packages/query-intent/skills/core/cancel-queries-and-consume-abort-signals/SKILL.md @@ -0,0 +1,182 @@ +--- +name: core/cancel-queries-and-consume-abort-signals +description: > + Use this when handling query cancellation, AbortSignal, cancelQueries, + CancelledError, rollback after consumed signals, fetch cancellation, axios + cancellation, and optimistic update overwrite prevention. +type: core +library: TanStack Query +library_version: '5.101.0' +requires: + - core/fetch-and-observe-queries +sources: + - TanStack/query:docs/framework/react/guides/query-cancellation.md + - TanStack/query:docs/reference/QueryClient.md + - TanStack/query:packages/query-core/src/retryer.ts +--- + +## Setup + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodo(todoId: string) { + return useQuery({ + queryKey: ['todo', todoId], + queryFn: async ({ signal }) => { + const response = await fetch(`/api/todos/${todoId}`, { signal }) + return response.json() as Promise<{ id: string; title: string }> + }, + }) +} +``` + +## Core Patterns + +### Cancel before optimistic writes + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +export async function prepareTodoWrite() { + await queryClient.cancelQueries({ queryKey: ['todos'] }) +} +``` + +### Consume one signal across nested fetches + +```ts +import { queryOptions } from '@tanstack/react-query' + +export const todosWithDetailsOptions = queryOptions({ + queryKey: ['todos-with-details'], + queryFn: async ({ signal }) => { + const todos = await fetch('/api/todos', { signal }).then( + (response) => response.json() as Promise>, + ) + return Promise.all( + todos.map((todo) => + fetch(`/api/todos/${todo.id}`, { signal }).then((response) => + response.json(), + ), + ), + ) + }, +}) +``` + +### Cancel by query key + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +export function cancelTodos() { + return queryClient.cancelQueries({ queryKey: ['todos'] }) +} +``` + +## Common Mistakes + +### HIGH Ignoring AbortSignal + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodo(id: string) { + return useQuery({ + queryKey: ['todo', id], + queryFn: async () => fetch(`/api/todos/${id}`).then((r) => r.json()), + }) +} +``` + +Correct: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodo(id: string) { + return useQuery({ + queryKey: ['todo', id], + queryFn: async ({ signal }) => + fetch(`/api/todos/${id}`, { signal }).then((r) => r.json()), + }) +} +``` + +TanStack Query provides an AbortSignal; the request is only cancelled if the query function consumes it. + +Source: TanStack/query:docs/framework/react/guides/query-cancellation.md + +### HIGH Assuming unmount cancels + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useReport() { + return useQuery({ + queryKey: ['report'], + queryFn: async () => fetch('/api/report').then((r) => r.json()), + }) +} +``` + +Correct: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useReport() { + return useQuery({ + queryKey: ['report'], + queryFn: async ({ signal }) => + fetch('/api/report', { signal }).then((r) => r.json()), + }) +} +``` + +Unused queries can continue and populate cache unless the signal is consumed. + +Source: TanStack/query:docs/framework/react/guides/query-cancellation.md + +### HIGH Suspense cancellation expected + +Wrong: + +```ts +import { useSuspenseQuery } from '@tanstack/react-query' + +export function useTodo(id: string) { + return useSuspenseQuery({ + queryKey: ['todo', id], + queryFn: async ({ signal }) => + fetch(`/api/todos/${id}`, { signal }).then((r) => r.json()), + }) +} +``` + +Correct: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodo(id: string) { + return useQuery({ + queryKey: ['todo', id], + queryFn: async ({ signal }) => + fetch(`/api/todos/${id}`, { signal }).then((r) => r.json()), + }) +} +``` + +Cancellation limitations apply to Suspense hooks; use non-suspense queries when cancellation behavior is required. + +Source: TanStack/query:docs/framework/react/guides/query-cancellation.md diff --git a/packages/query-intent/skills/core/concurrent-optimistic-updates/SKILL.md b/packages/query-intent/skills/core/concurrent-optimistic-updates/SKILL.md new file mode 100644 index 00000000000..1dd4cd7683e --- /dev/null +++ b/packages/query-intent/skills/core/concurrent-optimistic-updates/SKILL.md @@ -0,0 +1,144 @@ +--- +name: core/concurrent-optimistic-updates +description: > + Use this when multiple optimistic mutations can overlap: mutation variables, + submittedAt keys, scoped mutationKey filters, isMutating guards, cancellation, + rollback context, filtered list updates, and avoiding windows of inconsistent + UI during concurrent writes. +type: core +library: TanStack Query +library_version: '5.101.0' +requires: + - core/implement-optimistic-updates-and-cache-writes + - core/cancel-queries-and-consume-abort-signals +sources: + - https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query + - TanStack/query:docs/framework/react/guides/optimistic-updates.md + - TanStack/query:docs/framework/react/reference/useMutationState.md +--- + +## Core Patterns + +Concurrent optimistic updates need per-mutation identity and scoped invalidation. A rollback or invalidation from one mutation should not erase another mutation that is still pending. + +### Scope related mutations + +```tsx +const mutation = useMutation({ + mutationKey: ['items'], + mutationFn: toggleItem, + onMutate: async ({ id }) => { + await queryClient.cancelQueries({ queryKey: ['items', 'detail', id] }) + const previousItem = queryClient.getQueryData(['items', 'detail', id]) + + queryClient.setQueryData(['items', 'detail', id], (item) => + item ? { ...item, isActive: !item.isActive } : item, + ) + + return { previousItem } + }, + onError: (_error, variables, context) => { + queryClient.setQueryData( + ['items', 'detail', variables.id], + context?.previousItem, + ) + }, + onSettled: () => { + if (queryClient.isMutating({ mutationKey: ['items'] }) === 1) { + return queryClient.invalidateQueries({ queryKey: ['items'] }) + } + }, +}) +``` + +Use `queryClient.isMutating()` imperatively inside the callback so the count is current at the moment invalidation would run. + +### Render pending variables with stable keys + +```tsx +const pendingAdds = useMutationState({ + filters: { mutationKey: ['todos', 'add'], status: 'pending' }, + select: (mutation) => + `${mutation.state.submittedAt}:${mutation.state.variables}`, +}) +``` + +`submittedAt` distinguishes multiple pending mutations with the same variables. + +## Common Mistakes + +### CRITICAL One mutation invalidation reverts another optimistic update + +Wrong: + +```ts +onSettled: () => queryClient.invalidateQueries({ queryKey: ['items'] }) +``` + +Correct: + +```ts +onSettled: () => { + if (queryClient.isMutating({ mutationKey: ['items'] }) === 1) { + return queryClient.invalidateQueries({ queryKey: ['items'] }) + } +} +``` + +Skip intermediate invalidations while related optimistic mutations are still in flight. + +Source: https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query + +### HIGH Optimistic list update ignores current filters + +Wrong: + +```ts +queryClient.setQueryData(['items', 'list', filters], (items) => + items?.map((item) => (item.id === updated.id ? updated : item)), +) +``` + +Correct: + +```ts +queryClient.setQueryData(['items', 'list', filters], (items) => + items + ?.map((item) => (item.id === updated.id ? updated : item)) + .filter((item) => matchesFilters(item, filters)), +) +``` + +If the server would remove the updated item from the filtered list, the optimistic cache update should do the same. + +Source: https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query + +### HIGH Pending optimistic rows share unstable keys + +Wrong: + +```tsx +{ + variables.map((title) =>
  • {title}
  • ) +} +``` + +Correct: + +```tsx +const pending = useMutationState({ + filters: { mutationKey: ['todos', 'add'], status: 'pending' }, + select: (mutation) => ({ + variables: mutation.state.variables, + submittedAt: mutation.state.submittedAt, + }), +}) + +{ + pending.map((todo) =>
  • {todo.variables.title}
  • ) +} +``` + +Use mutation metadata to keep concurrent optimistic rows distinct. + +Source: TanStack/query:docs/framework/react/guides/optimistic-updates.md diff --git a/packages/query-intent/skills/core/coordinate-query-execution/SKILL.md b/packages/query-intent/skills/core/coordinate-query-execution/SKILL.md new file mode 100644 index 00000000000..8f3bbb08660 --- /dev/null +++ b/packages/query-intent/skills/core/coordinate-query-execution/SKILL.md @@ -0,0 +1,192 @@ +--- +name: core/coordinate-query-execution +description: > + Use this when coordinating enabled, skipToken, dependent queries, useQueries, + parallel queries, disabled queries, background fetching indicators, isFetching, + fetchStatus, and declarative refetch behavior. +type: core +library: TanStack Query +library_version: '5.101.0' +requires: + - core/fetch-and-observe-queries + - core/tune-defaults-freshness-retries-and-refetching +sources: + - TanStack/query:docs/framework/react/guides/dependent-queries.md + - TanStack/query:docs/framework/react/guides/parallel-queries.md + - TanStack/query:docs/framework/react/guides/disabling-queries.md + - TanStack/query:docs/framework/react/guides/background-fetching-indicators.md + - TanStack/query:docs/framework/react/reference/useQueries.md +--- + +## Setup + +```tsx +import { useQuery } from '@tanstack/react-query' + +export function Projects(props: { userId?: string }) { + const projects = useQuery({ + queryKey: ['projects', props.userId], + queryFn: async () => [{ id: 'p1', userId: props.userId }], + enabled: Boolean(props.userId), + }) + + return
    {JSON.stringify(projects.data ?? [])}
    +} +``` + +## Core Patterns + +### Gate by dependency + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useUserProjects(userId: string | undefined) { + return useQuery({ + queryKey: ['projects', userId], + queryFn: async () => [{ id: 'p1', userId }], + enabled: userId !== undefined, + }) +} +``` + +### Run dynamic parallel queries + +```ts +import { useQueries } from '@tanstack/react-query' + +export function useMessages(ids: Array) { + return useQueries({ + queries: ids.map((id) => ({ + queryKey: ['message', id], + queryFn: async () => ({ id, text: 'Hello' }), + })), + }) +} +``` + +### Show background refresh separately + +```tsx +import { useIsFetching } from '@tanstack/react-query' + +export function GlobalRefreshIndicator() { + const count = useIsFetching() + return count > 0 ?

    Refreshing

    : null +} +``` + +## Common Mistakes + +### HIGH Imperative disabled query + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useSearch(term: string) { + return useQuery({ + queryKey: ['search'], + queryFn: async () => [term], + enabled: false, + }) +} +``` + +Correct: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useSearch(term: string) { + return useQuery({ + queryKey: ['search', term], + queryFn: async () => [term], + enabled: term.length > 0, + }) +} +``` + +Permanent disabling opts out of normal invalidation and dependency-driven cache behavior. + +Source: TanStack/query:docs/framework/react/guides/disabling-queries.md + +### HIGH Duplicate useQueries keys + +Wrong: + +```ts +import { useQueries } from '@tanstack/react-query' + +export function useUsers(ids: Array) { + return useQueries({ + queries: ids.map((id) => ({ + queryKey: ['user'], + queryFn: async () => ({ id }), + })), + }) +} +``` + +Correct: + +```ts +import { useQueries } from '@tanstack/react-query' + +export function useUsers(ids: Array) { + return useQueries({ + queries: ids.map((id) => ({ + queryKey: ['user', id], + queryFn: async () => ({ id }), + })), + }) +} +``` + +Duplicate keys can share placeholder, selected, or cached data between different items. + +Source: TanStack/query:docs/framework/react/reference/useQueries.md + +### MEDIUM Full-page spinner on background refetch + +Wrong: + +```tsx +import { useQuery } from '@tanstack/react-query' + +export function Todos() { + const query = useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + }) + return query.isFetching ? ( +

    Loading

    + ) : ( +
    {JSON.stringify(query.data)}
    + ) +} +``` + +Correct: + +```tsx +import { useQuery } from '@tanstack/react-query' + +export function Todos() { + const query = useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + }) + return ( +
    +      {query.isFetching ? 'Refreshing ' : ''}
    +      {JSON.stringify(query.data ?? [])}
    +    
    + ) +} +``` + +`isFetching` also covers background refreshes after data is already available. + +Source: TanStack/query:docs/framework/react/guides/background-fetching-indicators.md diff --git a/packages/query-intent/skills/core/design-query-keys-and-options/SKILL.md b/packages/query-intent/skills/core/design-query-keys-and-options/SKILL.md new file mode 100644 index 00000000000..36b6dd98a54 --- /dev/null +++ b/packages/query-intent/skills/core/design-query-keys-and-options/SKILL.md @@ -0,0 +1,182 @@ +--- +name: core/design-query-keys-and-options +description: > + Use this when designing queryKey arrays, queryFn inputs, queryOptions, + infiniteQueryOptions, mutationOptions, skipToken, key factories, or TypeScript + inference for TanStack Query reads and writes. +type: core +library: TanStack Query +library_version: '5.101.0' +sources: + - TanStack/query:docs/framework/react/guides/query-keys.md + - TanStack/query:docs/framework/react/guides/query-options.md + - TanStack/query:docs/framework/react/typescript.md + - TanStack/query:docs/eslint/exhaustive-deps.md + - TanStack/query:docs/eslint/prefer-query-options.md +--- + +## Setup + +```ts +import { queryOptions } from '@tanstack/react-query' + +export function todoOptions(todoId: string) { + return queryOptions({ + queryKey: ['todo', todoId], + queryFn: async () => ({ id: todoId, title: 'Ship skills' }), + staleTime: 60_000, + }) +} +``` + +## Core Patterns + +### Put every query variable in the key + +```ts +import { queryOptions } from '@tanstack/react-query' + +export function projectsOptions(teamId: string, page: number) { + return queryOptions({ + queryKey: ['projects', teamId, page], + queryFn: async () => ({ teamId, page, items: [] as Array<{ id: string }> }), + }) +} +``` + +### Share one options factory + +```ts +import { QueryClient, useQuery, queryOptions } from '@tanstack/react-query' + +export const queryClient = new QueryClient() + +export function userOptions(userId: string) { + return queryOptions({ + queryKey: ['user', userId], + queryFn: async () => ({ id: userId, name: 'Tanner' }), + }) +} + +export function useUser(userId: string) { + return useQuery(userOptions(userId)) +} + +export function preloadUser(userId: string) { + return queryClient.ensureQueryData(userOptions(userId)) +} +``` + +### Use skipToken for typesafe absence + +```ts +import { skipToken, useQuery } from '@tanstack/react-query' + +export function useMaybeTodo(todoId: string | undefined) { + return useQuery({ + queryKey: ['todo', todoId], + queryFn: todoId ? async () => ({ id: todoId }) : skipToken, + }) +} +``` + +## Common Mistakes + +### CRITICAL Missing variable in key + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useProject(teamId: string) { + return useQuery({ + queryKey: ['project'], + queryFn: async () => ({ teamId }), + }) +} +``` + +Correct: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useProject(teamId: string) { + return useQuery({ + queryKey: ['project', teamId], + queryFn: async () => ({ teamId }), + }) +} +``` + +Query keys define cache identity; missing variables merge distinct data. + +Source: TanStack/query:docs/framework/react/guides/query-keys.md + +### HIGH Key and queryFn drift + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodo(todoId: string) { + return useQuery({ queryKey: ['todo'], queryFn: async () => ({ id: todoId }) }) +} +``` + +Correct: + +```ts +import { queryOptions, useQuery } from '@tanstack/react-query' + +function todoOptions(todoId: string) { + return queryOptions({ + queryKey: ['todo', todoId], + queryFn: async () => ({ id: todoId }), + }) +} + +export function useTodo(todoId: string) { + return useQuery(todoOptions(todoId)) +} +``` + +Options factories keep identity, fetch behavior, and inference together across hooks and prefetches. + +Source: TanStack/query:docs/eslint/prefer-query-options.md + +### HIGH skipToken inside suspense query + +Wrong: + +```ts +import { skipToken, useSuspenseQuery } from '@tanstack/react-query' + +export function useTodo(todoId: string | undefined) { + return useSuspenseQuery({ + queryKey: ['todo', todoId], + queryFn: todoId ? async () => ({ id: todoId }) : skipToken, + }) +} +``` + +Correct: + +```ts +import { skipToken, useQuery } from '@tanstack/react-query' + +export function useTodo(todoId: string | undefined) { + return useQuery({ + queryKey: ['todo', todoId], + queryFn: todoId ? async () => ({ id: todoId }) : skipToken, + }) +} +``` + +Suspense queries require a guaranteed query function and cannot be conditionally disabled. + +Source: TanStack/query:docs/framework/react/guides/suspense.md + +See also: `compositions/enforce-query-best-practices-with-eslint` for rules that enforce key and options mistakes. diff --git a/packages/query-intent/skills/core/fetch-and-observe-queries/SKILL.md b/packages/query-intent/skills/core/fetch-and-observe-queries/SKILL.md new file mode 100644 index 00000000000..f5eac95fa46 --- /dev/null +++ b/packages/query-intent/skills/core/fetch-and-observe-queries/SKILL.md @@ -0,0 +1,195 @@ +--- +name: core/fetch-and-observe-queries +description: > + Use this when reading server state with useQuery, useQueries, createQuery, + injectQuery, QueryObserver, QueryClient.fetchQuery, ensureQueryData, status, + fetchStatus, pending, paused, success, and error states. +type: core +library: TanStack Query +library_version: '5.101.0' +requires: + - lifecycle/setup-query-client-and-providers + - core/design-query-keys-and-options +sources: + - TanStack/query:docs/framework/react/guides/queries.md + - TanStack/query:docs/framework/react/guides/query-functions.md + - TanStack/query:docs/reference/QueryObserver.md + - TanStack/query:docs/reference/QueryClient.md + - TanStack/query:docs/eslint/no-void-query-fn.md +--- + +## Setup + +```tsx +import { useQuery } from '@tanstack/react-query' + +export function Todos() { + const query = useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1, title: 'Ship' }], + }) + + if (query.isPending) return

    Loading

    + if (query.isError) return

    {query.error.message}

    + return
    {JSON.stringify(query.data)}
    +} +``` + +## Core Patterns + +### Fetch imperatively through QueryClient + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +export function loadTodo(todoId: string) { + return queryClient.fetchQuery({ + queryKey: ['todo', todoId], + queryFn: async () => ({ id: todoId }), + }) +} +``` + +### Ensure cached data for loaders + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +export function ensureTodos() { + return queryClient.ensureQueryData({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + }) +} +``` + +### Distinguish status from fetchStatus + +```tsx +import { useQuery } from '@tanstack/react-query' + +export function TodoCount() { + const query = useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + }) + const label = + query.fetchStatus === 'fetching' && query.status === 'success' + ? 'Refreshing' + : 'Ready' + return ( +

    + {query.data?.length ?? 0} {label} +

    + ) +} +``` + +## Common Mistakes + +### CRITICAL Void query function + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodos() { + return useQuery({ + queryKey: ['todos'], + queryFn: async () => { + await Promise.resolve([{ id: 1 }]) + }, + }) +} +``` + +Correct: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodos() { + return useQuery({ + queryKey: ['todos'], + queryFn: async () => Promise.resolve([{ id: 1 }]), + }) +} +``` + +Query functions must return data; a missing return caches `undefined`. + +Source: TanStack/query:docs/eslint/no-void-query-fn.md + +### HIGH Only checking pending offline + +Wrong: + +```tsx +import { useQuery } from '@tanstack/react-query' + +export function Todos() { + const query = useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + networkMode: 'online', + }) + return query.isPending ?

    Loading

    :

    {query.fetchStatus}

    +} +``` + +Correct: + +```tsx +import { useQuery } from '@tanstack/react-query' + +export function Todos() { + const query = useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + networkMode: 'online', + }) + return query.fetchStatus === 'paused' ?

    Offline

    :

    {query.status}

    +} +``` + +`status` describes data state; `fetchStatus` describes whether fetching is paused, fetching, or idle. + +Source: TanStack/query:docs/framework/react/guides/queries.md + +### HIGH Using queries for writes + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useSaveTodo(title: string) { + return useQuery({ + queryKey: ['saveTodo', title], + queryFn: async () => ({ id: 1, title }), + }) +} +``` + +Correct: + +```ts +import { useMutation } from '@tanstack/react-query' + +export function useSaveTodo() { + return useMutation({ + mutationFn: async (title: string) => ({ id: 1, title }), + }) +} +``` + +Queries are for reads; writes need mutation lifecycle hooks, invalidation, rollback, and mutation state. + +Source: TanStack/query:docs/framework/react/guides/mutations.md + +See also: `core/tune-defaults-freshness-retries-and-refetching` for how defaults change status behavior. diff --git a/packages/query-intent/skills/core/implement-optimistic-updates-and-cache-writes/SKILL.md b/packages/query-intent/skills/core/implement-optimistic-updates-and-cache-writes/SKILL.md new file mode 100644 index 00000000000..6a13f22ace6 --- /dev/null +++ b/packages/query-intent/skills/core/implement-optimistic-updates-and-cache-writes/SKILL.md @@ -0,0 +1,178 @@ +--- +name: core/implement-optimistic-updates-and-cache-writes +description: > + Use this when implementing optimistic updates, onMutate rollback context, + cancelQueries before writes, setQueryData, setQueriesData, immutable cache writes, + mutation rollback, and offline-aware optimistic state. +type: core +library: TanStack Query +library_version: '5.101.0' +requires: + - core/write-mutations-and-invalidate-related-queries + - core/cancel-queries-and-consume-abort-signals +sources: + - TanStack/query:docs/framework/react/guides/optimistic-updates.md + - TanStack/query:docs/framework/react/guides/updates-from-mutation-responses.md + - TanStack/query:docs/framework/react/guides/query-cancellation.md + - TanStack/query:docs/framework/react/plugins/persistQueryClient.md +--- + +## Setup + +```ts +import { useMutation, useQueryClient } from '@tanstack/react-query' + +type Todo = { id: number; title: string } + +export function useOptimisticAddTodo() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async (title: string) => ({ id: Date.now(), title }), + onMutate: async (title) => { + await queryClient.cancelQueries({ queryKey: ['todos'] }) + const previous = queryClient.getQueryData>(['todos']) + queryClient.setQueryData>(['todos'], (old = []) => [ + ...old, + { id: -1, title }, + ]) + return { previous } + }, + onError: (_error, _title, context) => { + queryClient.setQueryData(['todos'], context?.previous) + }, + onSettled: async () => { + await queryClient.invalidateQueries({ queryKey: ['todos'] }) + }, + }) +} +``` + +## Core Patterns + +### Write mutation response into detail cache + +```ts +import { useMutation, useQueryClient } from '@tanstack/react-query' + +export function useUpdateTodo() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async (todo: { id: number; title: string }) => todo, + onSuccess: (todo) => queryClient.setQueryData(['todo', todo.id], todo), + }) +} +``` + +### Update lists immutably + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +queryClient.setQueryData>( + ['todos'], + (old = []) => + old.map((todo) => (todo.id === 1 ? { ...todo, title: 'Done' } : todo)), +) +``` + +### Roll back with context + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() +const previous = queryClient.getQueryData(['todos']) +queryClient.setQueryData(['todos'], [{ id: 1, title: 'Optimistic' }]) +queryClient.setQueryData(['todos'], previous) +``` + +## Common Mistakes + +### CRITICAL Optimistic write without cancellation + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() +queryClient.setQueryData(['todos'], [{ id: 1, title: 'Optimistic' }]) +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() +await queryClient.cancelQueries({ queryKey: ['todos'] }) +queryClient.setQueryData(['todos'], [{ id: 1, title: 'Optimistic' }]) +``` + +An in-flight refetch can overwrite an optimistic write unless it is cancelled first. + +Source: TanStack/query:docs/framework/react/guides/optimistic-updates.md + +### CRITICAL In-place mutation + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() +queryClient.setQueryData>( + ['todos'], + (old = []) => { + old[0].done = true + return old + }, +) +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() +queryClient.setQueryData>( + ['todos'], + (old = []) => + old.map((todo) => (todo.id === 1 ? { ...todo, done: true } : todo)), +) +``` + +Cache updates must be immutable so observers can detect and share changes correctly. + +Source: TanStack/query:docs/framework/react/guides/updates-from-mutation-responses.md + +### HIGH Persisted persister loses optimistic mutation + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient() +queryClient.setMutationDefaults(['updateTodo'], {}) +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient() +queryClient.setMutationDefaults(['updateTodo'], { + mutationFn: async (todo: { id: number; title: string }) => todo, +}) +``` + +Paused persisted mutations need a default mutationFn after hydration because functions cannot be serialized. + +Source: TanStack/query:docs/framework/react/plugins/persistQueryClient.md + +See also: `core/cancel-queries-and-consume-abort-signals` before optimistic writes. diff --git a/packages/query-intent/skills/core/paginate-and-build-infinite-queries/SKILL.md b/packages/query-intent/skills/core/paginate-and-build-infinite-queries/SKILL.md new file mode 100644 index 00000000000..51c5bbcb4bc --- /dev/null +++ b/packages/query-intent/skills/core/paginate-and-build-infinite-queries/SKILL.md @@ -0,0 +1,191 @@ +--- +name: core/paginate-and-build-infinite-queries +description: > + Use this when implementing pagination, lagged queries, placeholderData, + keepPreviousData migration, useInfiniteQuery, initialPageParam, + getNextPageParam, getPreviousPageParam, maxPages, pages, or pageParams. +type: core +library: TanStack Query +library_version: '5.101.0' +requires: + - core/design-query-keys-and-options + - core/fetch-and-observe-queries +sources: + - TanStack/query:docs/framework/react/guides/paginated-queries.md + - TanStack/query:docs/framework/react/guides/infinite-queries.md + - TanStack/query:docs/framework/react/reference/useInfiniteQuery.md + - TanStack/query:docs/reference/InfiniteQueryObserver.md + - TanStack/query:docs/eslint/infinite-query-property-order.md +--- + +## Setup + +```ts +import { keepPreviousData, useQuery } from '@tanstack/react-query' + +export function useProjects(page: number) { + return useQuery({ + queryKey: ['projects', page], + queryFn: async () => ({ page, projects: [{ id: page }] }), + placeholderData: keepPreviousData, + }) +} +``` + +## Core Patterns + +### Build an infinite query + +```ts +import { useInfiniteQuery } from '@tanstack/react-query' + +export function useFeed() { + return useInfiniteQuery({ + queryKey: ['feed'], + queryFn: async ({ pageParam }) => ({ + nextCursor: pageParam + 1, + items: [{ id: pageParam }], + }), + initialPageParam: 0, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }) +} +``` + +### Bound memory with maxPages + +```ts +import { useInfiniteQuery } from '@tanstack/react-query' + +export function useBoundedFeed() { + return useInfiniteQuery({ + queryKey: ['feed'], + queryFn: async ({ pageParam }) => ({ + nextCursor: pageParam + 1, + items: [{ id: pageParam }], + }), + initialPageParam: 0, + getNextPageParam: (lastPage) => lastPage.nextCursor, + maxPages: 5, + }) +} +``` + +### Preserve infinite data shape + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +queryClient.setQueryData(['feed'], { + pages: [{ items: [{ id: 1 }], nextCursor: 2 }], + pageParams: [1], +}) +``` + +## Common Mistakes + +### CRITICAL Missing initialPageParam + +Wrong: + +```ts +import { useInfiniteQuery } from '@tanstack/react-query' + +export function useFeed() { + return useInfiniteQuery({ + queryKey: ['feed'], + queryFn: async () => ({ items: [] }), + getNextPageParam: () => 1, + }) +} +``` + +Correct: + +```ts +import { useInfiniteQuery } from '@tanstack/react-query' + +export function useFeed() { + return useInfiniteQuery({ + queryKey: ['feed'], + queryFn: async ({ pageParam }) => ({ items: [pageParam] }), + initialPageParam: 0, + getNextPageParam: () => 1, + }) +} +``` + +v5 requires an explicit initial page param so pageParams can be serialized and inferred. + +Source: TanStack/query:docs/framework/react/guides/infinite-queries.md + +### HIGH Overlapping infinite fetches + +Wrong: + +```tsx +import { useInfiniteQuery } from '@tanstack/react-query' + +export function Feed() { + const query = useInfiniteQuery({ + queryKey: ['feed'], + queryFn: async ({ pageParam }) => ({ nextCursor: pageParam + 1 }), + initialPageParam: 0, + getNextPageParam: (page) => page.nextCursor, + }) + return +} +``` + +Correct: + +```tsx +import { useInfiniteQuery } from '@tanstack/react-query' + +export function Feed() { + const query = useInfiniteQuery({ + queryKey: ['feed'], + queryFn: async ({ pageParam }) => ({ nextCursor: pageParam + 1 }), + initialPageParam: 0, + getNextPageParam: (page) => page.nextCursor, + }) + return ( + + ) +} +``` + +Only one ongoing fetch should update an infinite query cache at a time unless explicitly overridden. + +Source: TanStack/query:docs/framework/react/guides/infinite-queries.md + +### HIGH Broken pages shape + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() +queryClient.setQueryData(['feed'], [{ id: 1 }]) +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() +queryClient.setQueryData(['feed'], { pages: [[{ id: 1 }]], pageParams: [0] }) +``` + +Infinite query data must keep `pages` and `pageParams`; refetches expect that shape. + +Source: TanStack/query:docs/framework/react/guides/infinite-queries.md diff --git a/packages/query-intent/skills/core/seed-placeholder-select-and-transform-data/SKILL.md b/packages/query-intent/skills/core/seed-placeholder-select-and-transform-data/SKILL.md new file mode 100644 index 00000000000..ba80cd170c5 --- /dev/null +++ b/packages/query-intent/skills/core/seed-placeholder-select-and-transform-data/SKILL.md @@ -0,0 +1,188 @@ +--- +name: core/seed-placeholder-select-and-transform-data +description: > + Use this when choosing initialData, initialDataUpdatedAt, placeholderData, + keepPreviousData, select, data transformation, cache seeding, detail-from-list + seeding, or derived query data. +type: core +library: TanStack Query +library_version: '5.101.0' +requires: + - core/design-query-keys-and-options + - core/fetch-and-observe-queries +sources: + - TanStack/query:docs/framework/react/guides/initial-query-data.md + - TanStack/query:docs/framework/react/guides/placeholder-query-data.md + - TanStack/query:docs/framework/react/guides/render-optimizations.md + - TanStack/query:docs/framework/react/reference/queryOptions.md +--- + +## Setup + +```ts +import { keepPreviousData, useQuery } from '@tanstack/react-query' + +export function useProjects(page: number) { + return useQuery({ + queryKey: ['projects', page], + queryFn: async () => ({ page, items: [{ id: page }] }), + placeholderData: keepPreviousData, + }) +} +``` + +## Core Patterns + +### Seed detail data from a list + +```ts +import { QueryClient, useQuery } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +export function useTodo(todoId: number) { + return useQuery({ + queryKey: ['todo', todoId], + queryFn: async () => ({ id: todoId, title: 'Fresh' }), + initialData: () => + queryClient + .getQueryData>(['todos']) + ?.find((todo) => todo.id === todoId), + }) +} +``` + +### Select the smallest shape + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodoCount() { + return useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }, { id: 2 }], + select: (todos) => todos.length, + }) +} +``` + +### Transform in the query function for cache-wide shape + +```ts +import { queryOptions } from '@tanstack/react-query' + +export const todosOptions = queryOptions({ + queryKey: ['todos'], + queryFn: async () => + [{ id: 1, title: 'ship' }].map((todo) => ({ + ...todo, + title: todo.title.toUpperCase(), + })), +}) +``` + +## Common Mistakes + +### HIGH initialData overwrite assumption + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodo() { + return useQuery({ + queryKey: ['todo', 1], + queryFn: async () => ({ id: 1, title: 'Server' }), + initialData: { id: 1, title: 'Always' }, + }) +} +``` + +Correct: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodo() { + return useQuery({ + queryKey: ['todo', 1], + queryFn: async () => ({ id: 1, title: 'Server' }), + placeholderData: { id: 1, title: 'Temporary' }, + }) +} +``` + +`initialData` is persisted to cache as real data; placeholder data is observer-local. + +Source: TanStack/query:docs/framework/react/guides/initial-query-data.md + +### HIGH v4 keepPreviousData option + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function usePage(page: number) { + return useQuery({ + queryKey: ['page', page], + queryFn: async () => ({ page }), + keepPreviousData: true, + }) +} +``` + +Correct: + +```ts +import { keepPreviousData, useQuery } from '@tanstack/react-query' + +export function usePage(page: number) { + return useQuery({ + queryKey: ['page', page], + queryFn: async () => ({ page }), + placeholderData: keepPreviousData, + }) +} +``` + +v5 replaced the `keepPreviousData` option with `placeholderData: keepPreviousData`. + +Source: TanStack/query:docs/framework/react/guides/migrating-to-v5.md + +### MEDIUM Throwing in select + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useFirstTodo() { + return useQuery({ + queryKey: ['todos'], + queryFn: async () => [] as Array<{ id: number }>, + select: (todos) => todos[0].id, + }) +} +``` + +Correct: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useFirstTodo() { + return useQuery({ + queryKey: ['todos'], + queryFn: async () => [] as Array<{ id: number }>, + select: (todos) => todos[0]?.id ?? null, + }) +} +``` + +`select` transforms successful data; it is not the right place to model fetch errors. + +Source: TanStack/query:docs/framework/react/guides/render-optimizations.md + +See also: `lifecycle/ssr-hydration-and-streaming` for SSR tradeoffs compared to hydration. diff --git a/packages/query-intent/skills/core/selectors-and-derived-state/SKILL.md b/packages/query-intent/skills/core/selectors-and-derived-state/SKILL.md new file mode 100644 index 00000000000..ee12eca73d1 --- /dev/null +++ b/packages/query-intent/skills/core/selectors-and-derived-state/SKILL.md @@ -0,0 +1,153 @@ +--- +name: core/selectors-and-derived-state +description: > + Use this when selecting, transforming, or deriving data from TanStack Query: + select functions, selector memoization, fine-grained subscriptions, structural + sharing, queryOptions composition with select, and deriving client state from + server state instead of syncing it through effects. +type: core +library: TanStack Query +library_version: '5.101.0' +requires: + - core/seed-placeholder-select-and-transform-data + - framework/shape-data-and-render-efficiently +sources: + - https://tkdodo.eu/blog/react-query-selectors-supercharged + - https://tkdodo.eu/blog/deriving-client-state-from-server-state + - TanStack/query:docs/framework/react/guides/render-optimizations.md + - TanStack/query:docs/framework/react/reference/queryOptions.md +--- + +## Core Patterns + +Use `select` to subscribe a component to the data shape it actually needs while keeping the full response in the cache. + +### Select a stable slice + +```tsx +import { queryOptions, useSuspenseQuery } from '@tanstack/react-query' + +const productOptions = (id: string) => + queryOptions({ + queryKey: ['product', id], + queryFn: () => fetchProduct(id), + }) + +function ProductTitle({ id }: { id: string }) { + const title = useSuspenseQuery({ + ...productOptions(id), + select: (product) => product.title, + }) + + return

    {title.data}

    +} +``` + +### Stabilize expensive selectors + +```tsx +const selectAverageRating = (products: Array) => + expensiveAverage(products) + +function ProductSummary({ filters }: { filters: ProductFilters }) { + return useSuspenseQuery({ + ...productsOptions(filters), + select: selectAverageRating, + }) +} +``` + +Use `React.useCallback` when the selector closes over component props. Move it outside the component when it has no dependencies. + +### Derive client state instead of syncing it + +```tsx +const selectedUser = usersQuery.data?.find((user) => user.id === selectedUserId) +const visibleSelectedUserId = selectedUser ? selectedUserId : null +``` + +When server data changes, derived values update naturally during render. + +## Common Mistakes + +### HIGH Syncing derived state through an effect + +Wrong: + +```tsx +React.useEffect(() => { + if (!users?.some((user) => user.id === selectedUserId)) { + setSelectedUserId(null) + } +}, [users, selectedUserId]) +``` + +Correct: + +```tsx +const selectedUser = users?.find((user) => user.id === selectedUserId) +const visibleSelectedUserId = selectedUser ? selectedUserId : null +``` + +Prefer deriving client state from server state during render when no side effect is required. + +Source: https://tkdodo.eu/blog/deriving-client-state-from-server-state + +### MEDIUM Inline expensive select reruns on unrelated renders + +Wrong: + +```tsx +useSuspenseQuery({ + ...productsOptions(filters), + select: (data) => expensiveSuperTransformation(data), +}) +``` + +Correct: + +```tsx +const selectProducts = (data: Array) => + expensiveSuperTransformation(data) + +useSuspenseQuery({ + ...productsOptions(filters), + select: selectProducts, +}) +``` + +TanStack Query reruns `select` when data changes or the selector function identity changes. + +Source: https://tkdodo.eu/blog/react-query-selectors-supercharged + +### MEDIUM Using select to throw domain errors + +Wrong: + +```tsx +useQuery({ + queryKey: ['todos'], + queryFn: fetchTodos, + select: (todos) => { + if (!todos.length) throw new Error('No todos') + return todos + }, +}) +``` + +Correct: + +```tsx +useQuery({ + queryKey: ['todos'], + queryFn: async () => { + const todos = await fetchTodos() + if (!todos.length) throw new Error('No todos') + return todos + }, +}) +``` + +`select` transforms successful data. Query errors belong in the query function. + +Source: TanStack/query:docs/framework/react/guides/render-optimizations.md diff --git a/packages/query-intent/skills/core/tune-defaults-freshness-retries-and-refetching/SKILL.md b/packages/query-intent/skills/core/tune-defaults-freshness-retries-and-refetching/SKILL.md new file mode 100644 index 00000000000..b100963d205 --- /dev/null +++ b/packages/query-intent/skills/core/tune-defaults-freshness-retries-and-refetching/SKILL.md @@ -0,0 +1,172 @@ +--- +name: core/tune-defaults-freshness-retries-and-refetching +description: > + Use this when configuring staleTime, gcTime, cache lifetime, retry, retryDelay, + refetchOnWindowFocus, refetchOnReconnect, refetchInterval, networkMode, + focusManager, onlineManager, timeoutManager, or QueryClient defaultOptions. +type: core +library: TanStack Query +library_version: '5.101.0' +requires: + - core/fetch-and-observe-queries +sources: + - TanStack/query:docs/framework/react/guides/important-defaults.md + - TanStack/query:docs/framework/react/guides/caching.md + - TanStack/query:docs/framework/react/guides/query-retries.md + - TanStack/query:docs/framework/react/guides/window-focus-refetching.md + - TanStack/query:docs/framework/react/guides/network-mode.md + - TanStack/query:docs/reference/focusManager.md + - TanStack/query:docs/reference/onlineManager.md + - TanStack/query:docs/reference/timeoutManager.md +--- + +## Setup + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60_000, + gcTime: 10 * 60_000, + retry: 2, + refetchOnWindowFocus: true, + }, + }, +}) +``` + +## Core Patterns + +### Set freshness close to the data source + +```ts +import { queryOptions } from '@tanstack/react-query' + +export const settingsOptions = queryOptions({ + queryKey: ['settings'], + queryFn: async () => ({ theme: 'system' }), + staleTime: 5 * 60_000, +}) +``` + +### Disable retries in tests + +```ts +import { QueryClient } from '@tanstack/react-query' + +export function createTestQueryClient() { + return new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }) +} +``` + +### Use networkMode deliberately + +```ts +import { queryOptions } from '@tanstack/react-query' + +export const metricsOptions = queryOptions({ + queryKey: ['metrics'], + queryFn: async () => ({ count: 1 }), + networkMode: 'online', +}) +``` + +## Common Mistakes + +### HIGH Confusing gcTime with freshness + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useProfile() { + return useQuery({ + queryKey: ['profile'], + queryFn: async () => ({ name: 'Tanner' }), + gcTime: 60_000, + }) +} +``` + +Correct: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useProfile() { + return useQuery({ + queryKey: ['profile'], + queryFn: async () => ({ name: 'Tanner' }), + staleTime: 60_000, + }) +} +``` + +`gcTime` controls unused cache retention; `staleTime` controls whether cached data is considered fresh. + +Source: TanStack/query:docs/framework/react/guides/important-defaults.md + +### HIGH static staleTime blocks invalidation expectations + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodos() { + return useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + staleTime: 'static', + }) +} +``` + +Correct: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodos() { + return useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + staleTime: 60_000, + }) +} +``` + +`staleTime: 'static'` opts out of refetching even when the query is invalidated. + +Source: TanStack/query:docs/framework/react/guides/important-defaults.md + +### HIGH Tests hang on retries + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient() +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, +}) +``` + +Default retries add delay and can make failing tests wait before surfacing errors. + +Source: TanStack/query:docs/framework/react/guides/testing.md + +See also: `compositions/persist-offline-and-restore-caches` for persistence rules that depend on gcTime and networkMode. diff --git a/packages/query-intent/skills/core/understand-query-internals-and-observers/SKILL.md b/packages/query-intent/skills/core/understand-query-internals-and-observers/SKILL.md new file mode 100644 index 00000000000..0b9ebd6132c --- /dev/null +++ b/packages/query-intent/skills/core/understand-query-internals-and-observers/SKILL.md @@ -0,0 +1,114 @@ +--- +name: core/understand-query-internals-and-observers +description: > + Use this when explaining or debugging TanStack Query internals: QueryClient, + QueryCache, MutationCache, Query, QueryObserver, active versus inactive + queries, observer-level options, subscriptions, stale timers, and why loaders + or imperative cache reads are not the same as observed queries. +type: core +library: TanStack Query +library_version: '5.101.0' +requires: + - core/fetch-and-observe-queries +sources: + - https://tkdodo.eu/blog/inside-react-query + - https://tkdodo.eu/blog/react-query-selectors-supercharged + - TanStack/query:docs/reference/QueryClient.md + - TanStack/query:docs/reference/QueryCache.md + - TanStack/query:docs/reference/QueryObserver.md + - TanStack/query:docs/reference/MutationCache.md +--- + +## Mental Model + +`QueryClient` owns the caches. `QueryCache` stores `Query` instances. Hooks and adapter APIs create `QueryObserver` instances that subscribe components to one query. Observer-level options like `select`, `staleTime`, polling, and tracked result access shape what each component sees. + +An inactive query can exist in the cache without active observers. It can be read imperatively, hydrated, invalidated, or garbage collected, but it is not the same as a component actively observing query state. + +## Core Patterns + +### Use observers for UI reads + +```tsx +function TodoPage({ id }: { id: string }) { + const todo = useQuery({ + queryKey: ['todo', id], + queryFn: () => fetchTodo(id), + }) + + if (todo.isPending) return

    Loading...

    + if (todo.isError) return

    {todo.error.message}

    + return

    {todo.data.title}

    +} +``` + +### Use QueryClient for cache orchestration + +```ts +await queryClient.ensureQueryData(todoOptions(id)) +queryClient.invalidateQueries({ queryKey: ['todo', id] }) +queryClient.setQueryData(['todo', id], (old) => old && { ...old, title }) +``` + +## Common Mistakes + +### HIGH Treating cache presence as active usage + +Wrong: + +```tsx +const todo = queryClient.getQueryData(['todo', id]) +return +``` + +Correct: + +```tsx +const todo = useQuery(todoOptions(id)) +return +``` + +UI reads should create observers so invalidation, refetch triggers, stale status, and garbage collection behave as expected. + +Source: https://tkdodo.eu/blog/inside-react-query + +### MEDIUM Expecting one query to have one option set + +Wrong: + +```ts +// assume staleTime is stored only on the Query +queryClient.getQueryCache().find({ queryKey })?.options.staleTime +``` + +Correct: + +```ts +const query = queryClient.getQueryCache().find({ queryKey }) +const observerStaleTimes = query?.observers.map( + (observer) => observer.options.staleTime, +) +``` + +Several options are observer-level, so multiple components can observe the same query with different selectors or freshness behavior. + +Source: https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations + +### MEDIUM Debugging without checking observer count + +Wrong: + +```ts +queryClient.invalidateQueries({ queryKey: ['todos'] }) +// expect every cached todo query to refetch immediately +``` + +Correct: + +```ts +queryClient.invalidateQueries({ queryKey: ['todos'], refetchType: 'active' }) +``` + +Inactive queries are not automatically the same as mounted queries. Check observer count in devtools when invalidation or garbage collection looks surprising. + +Source: https://tkdodo.eu/blog/inside-react-query diff --git a/packages/query-intent/skills/core/write-mutations-and-invalidate-related-queries/SKILL.md b/packages/query-intent/skills/core/write-mutations-and-invalidate-related-queries/SKILL.md new file mode 100644 index 00000000000..316f6027916 --- /dev/null +++ b/packages/query-intent/skills/core/write-mutations-and-invalidate-related-queries/SKILL.md @@ -0,0 +1,184 @@ +--- +name: core/write-mutations-and-invalidate-related-queries +description: > + Use this when writing server state with useMutation, mutationFn, mutate, + mutateAsync, mutationKey, useMutationState, invalidateQueries, setMutationDefaults, + awaited invalidation, and mutation callbacks. +type: core +library: TanStack Query +library_version: '5.101.0' +requires: + - core/design-query-keys-and-options + - core/fetch-and-observe-queries +sources: + - TanStack/query:docs/framework/react/guides/mutations.md + - TanStack/query:docs/framework/react/guides/query-invalidation.md + - TanStack/query:docs/framework/react/guides/invalidations-from-mutations.md + - TanStack/query:docs/framework/react/guides/updates-from-mutation-responses.md + - TanStack/query:docs/framework/react/reference/useMutation.md + - TanStack/query:docs/framework/react/reference/useMutationState.md + - TanStack/query:docs/reference/MutationCache.md +--- + +## Setup + +```ts +import { useMutation, useQueryClient } from '@tanstack/react-query' + +export function useAddTodo() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async (title: string) => ({ id: Date.now(), title }), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['todos'] }) + }, + }) +} +``` + +## Core Patterns + +### Update from mutation response + +```ts +import { useMutation, useQueryClient } from '@tanstack/react-query' + +export function useSaveTodo() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async (todo: { id: string; title: string }) => todo, + onSuccess: (todo) => { + queryClient.setQueryData(['todo', todo.id], todo) + }, + }) +} +``` + +### Track related mutations + +```ts +import { useMutationState } from '@tanstack/react-query' + +export function usePendingTodoTitles() { + return useMutationState({ + filters: { mutationKey: ['addTodo'], status: 'pending' }, + select: (mutation) => mutation.state.variables as string, + }) +} +``` + +### Scope defaults by mutation key + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient() + +queryClient.setMutationDefaults(['addTodo'], { + mutationFn: async (title: string) => ({ id: Date.now(), title }), +}) +``` + +## Common Mistakes + +### HIGH Multiple mutate arguments + +Wrong: + +```ts +import { useMutation } from '@tanstack/react-query' + +export function useUpdateTodo() { + return useMutation({ + mutationFn: async (input: { id: string; title: string }) => input, + }) +} + +useUpdateTodo().mutate('1', 'Ship') +``` + +Correct: + +```ts +import { useMutation } from '@tanstack/react-query' + +export function useUpdateTodo() { + return useMutation({ + mutationFn: async (input: { id: string; title: string }) => input, + }) +} + +useUpdateTodo().mutate({ id: '1', title: 'Ship' }) +``` + +Mutation variables are one value; pass an object when multiple fields are needed. + +Source: TanStack/query:docs/framework/react/guides/mutations.md + +### HIGH Per-call callback after unmount + +Wrong: + +```ts +import { useMutation } from '@tanstack/react-query' + +export function useSave() { + return useMutation({ mutationFn: async (title: string) => title }) +} + +useSave().mutate('Ship', { onSuccess: () => console.log('saved') }) +``` + +Correct: + +```ts +import { useMutation } from '@tanstack/react-query' + +export function useSave() { + return useMutation({ + mutationFn: async (title: string) => title, + onSuccess: () => console.log('saved'), + }) +} +``` + +Hook-level callbacks are tied to the mutation lifecycle; per-call callbacks may not run if the observer unmounts. + +Source: TanStack/query:docs/framework/react/guides/mutations.md + +### HIGH Not awaiting invalidation + +Wrong: + +```ts +import { useMutation, useQueryClient } from '@tanstack/react-query' + +export function useAddTodo() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async (title: string) => title, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), + }) +} +``` + +Correct: + +```ts +import { useMutation, useQueryClient } from '@tanstack/react-query' + +export function useAddTodo() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async (title: string) => title, + onSuccess: async () => + queryClient.invalidateQueries({ queryKey: ['todos'] }), + }) +} +``` + +Returning the invalidation promise keeps the mutation pending until dependent data is refreshed. + +Source: TanStack/query:docs/framework/react/guides/invalidations-from-mutations.md + +See also: `core/implement-optimistic-updates-and-cache-writes` for mutation side effects that update the cache before the server returns. diff --git a/packages/query-intent/skills/framework/debug-with-devtools/SKILL.md b/packages/query-intent/skills/framework/debug-with-devtools/SKILL.md new file mode 100644 index 00000000000..d77a764affb --- /dev/null +++ b/packages/query-intent/skills/framework/debug-with-devtools/SKILL.md @@ -0,0 +1,169 @@ +--- +name: framework/debug-with-devtools +description: > + Use this when adding, lazy-loading, or debugging TanStack Query devtools: + ReactQueryDevtools, VueQueryDevtools, SolidQueryDevtools, SvelteQueryDevtools, + Angular devtools panel, embedded panels, production imports, cache inspection, + and offline misconceptions. +type: framework +library: TanStack Query +framework: cross-adapter +library_version: '5.101.0' +requires: + - lifecycle/setup-query-client-and-providers +sources: + - TanStack/query:docs/framework/react/devtools.md + - TanStack/query:docs/framework/preact/devtools.md + - TanStack/query:docs/framework/vue/devtools.md + - TanStack/query:docs/framework/solid/devtools.md + - TanStack/query:docs/framework/svelte/devtools.md + - TanStack/query:docs/framework/angular/devtools.md +--- + +This skill builds on `lifecycle/setup-query-client-and-providers`. + +## Setup + +```tsx +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' + +export function Devtools() { + return +} +``` + +## Hooks and Components + +### Lazy-load production devtools + +```tsx +import * as React from 'react' + +const ReactQueryDevtoolsProduction = React.lazy(() => + import('@tanstack/react-query-devtools/production').then((module) => ({ + default: module.ReactQueryDevtools, + })), +) + +export function LazyDevtools() { + return ( + + + + ) +} +``` + +### Embed a panel + +```tsx +import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' + +export function DevtoolsPanel() { + return +} +``` + +### Use adapter-specific packages + +```ts +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { VueQueryDevtools } from '@tanstack/vue-query-devtools' + +export const devtools = { ReactQueryDevtools, VueQueryDevtools } +``` + +## Common Mistakes + +### MEDIUM Eager production devtools + +Wrong: + +```tsx +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' + +export function AppDevtools() { + return +} +``` + +Correct: + +```tsx +import * as React from 'react' + +const Devtools = React.lazy(() => + import('@tanstack/react-query-devtools/production').then((module) => ({ + default: module.ReactQueryDevtools, + })), +) +export function AppDevtools() { + return ( + + + + ) +} +``` + +Production devtools should be lazy-loaded from the production entry. + +Source: TanStack/query:docs/framework/react/devtools.md + +### MEDIUM Mock offline misconception + +Wrong: + +```tsx +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' + +export function Devtools() { + return +} +``` + +Correct: + +```ts +import { onlineManager } from '@tanstack/react-query' + +export function setOfflineForTest() { + onlineManager.setOnline(false) +} +``` + +Devtools inspect state; use Query managers or browser tooling to model network state. + +Source: TanStack/query:docs/reference/onlineManager.md + +### HIGH Devtools outside provider + +Wrong: + +```tsx +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' + +export function Root() { + return +} +``` + +Correct: + +```tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' + +const queryClient = new QueryClient() +export function Root() { + return ( + + + + ) +} +``` + +Devtools need the same QueryClient context as the app. + +Source: TanStack/query:docs/framework/react/devtools.md diff --git a/packages/query-intent/skills/framework/handle-status-and-errors/SKILL.md b/packages/query-intent/skills/framework/handle-status-and-errors/SKILL.md new file mode 100644 index 00000000000..f831cac58b1 --- /dev/null +++ b/packages/query-intent/skills/framework/handle-status-and-errors/SKILL.md @@ -0,0 +1,131 @@ +--- +name: framework/handle-status-and-errors +description: > + Use this when designing TanStack Query loading, empty, stale, background error, + retry, toast, throwOnError, and Error Boundary flows. Covers status versus + fetchStatus, stale data after failed refetches, global QueryCache or + MutationCache error callbacks, and local versus boundary-level error handling. +type: framework +library: TanStack Query +library_version: '5.101.0' +requires: + - core/fetch-and-observe-queries + - framework/use-suspense-and-error-boundaries +sources: + - https://tkdodo.eu/blog/status-checks-in-react-query + - https://tkdodo.eu/blog/react-query-error-handling + - TanStack/query:docs/framework/react/guides/queries.md + - TanStack/query:docs/framework/react/reference/QueryErrorResetBoundary.md +--- + +## Core Patterns + +Prefer data-first rendering when stale data is useful. A failed background refetch can produce `isError` while `data` is still available. + +```tsx +const todos = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) + +if (todos.data) + return +if (todos.isPending) return +if (todos.isError) return +return null +``` + +Use `throwOnError` when render-time Error Boundaries should own the fallback: + +```tsx +useQuery({ + queryKey: ['todos'], + queryFn: fetchTodos, + throwOnError: (error) => error.status >= 500, +}) +``` + +Use global cache callbacks for cross-cutting notifications: + +```ts +import { QueryCache, QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: (error, query) => { + if (query.state.data !== undefined) showToast(error.message) + }, + }), +}) +``` + +## Common Mistakes + +### HIGH Hiding stale data on background error + +Wrong: + +```tsx +if (query.isError) return +if (query.data) return +``` + +Correct: + +```tsx +if (query.data) + return ( + + ) +if (query.isError) return +``` + +Background refetch failures should not necessarily erase already-rendered data. + +Source: https://tkdodo.eu/blog/status-checks-in-react-query + +### HIGH Sending validation errors to a global boundary + +Wrong: + +```ts +useMutation({ mutationFn: submitForm, throwOnError: true }) +``` + +Correct: + +```ts +useMutation({ + mutationFn: submitForm, + throwOnError: (error) => error.status >= 500, +}) +``` + +Handle expected 4xx validation errors near the form. Send unexpected server failures to the boundary. + +Source: https://tkdodo.eu/blog/react-query-error-handling + +### MEDIUM Duplicating toast notifications per observer + +Wrong: + +```ts +useQuery({ + queryKey: ['todos'], + queryFn: fetchTodos, + onError: toastError, +}) +``` + +Correct: + +```ts +new QueryClient({ + queryCache: new QueryCache({ + onError: (error, query) => { + if (query.state.data !== undefined) toastError(error) + }, + }), +}) +``` + +Observer-level callbacks can duplicate notifications across components. Use cache-level callbacks for global side effects. + +Source: https://tkdodo.eu/blog/react-query-error-handling diff --git a/packages/query-intent/skills/framework/shape-data-and-render-efficiently/SKILL.md b/packages/query-intent/skills/framework/shape-data-and-render-efficiently/SKILL.md new file mode 100644 index 00000000000..d956b327c0a --- /dev/null +++ b/packages/query-intent/skills/framework/shape-data-and-render-efficiently/SKILL.md @@ -0,0 +1,184 @@ +--- +name: framework/shape-data-and-render-efficiently +description: > + Use this when optimizing TanStack Query rendering with structuralSharing, + tracked properties, select, notifyOnChangeProps, stable hook deps, immutable + data, Vue reactivity, and no-rest-destructuring or no-unstable-deps lint rules. +type: framework +library: TanStack Query +framework: cross-adapter +library_version: '5.101.0' +requires: + - core/fetch-and-observe-queries + - core/seed-placeholder-select-and-transform-data +sources: + - TanStack/query:docs/framework/react/guides/render-optimizations.md + - TanStack/query:docs/eslint/no-rest-destructuring.md + - TanStack/query:docs/eslint/no-unstable-deps.md + - TanStack/query:docs/framework/vue/reactivity.md +--- + +This skill builds on `core/fetch-and-observe-queries` and `core/seed-placeholder-select-and-transform-data`. + +## Setup + +```tsx +import { useQuery } from '@tanstack/react-query' + +export function TodoCount() { + const { data } = useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }, { id: 2 }], + select: (todos) => todos.length, + }) + return

    {data ?? 0}

    +} +``` + +## Hooks and Components + +### Destructure only used result fields + +```tsx +import { useQuery } from '@tanstack/react-query' + +export function Todos() { + const { data, isFetching } = useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + }) + return ( +
    +      {isFetching ? 'Refreshing ' : ''}
    +      {JSON.stringify(data ?? [])}
    +    
    + ) +} +``` + +### Keep Vue query data immutable + +```ts +import { ref } from 'vue' + +export function editableTodo(todo: { id: number; title: string }) { + return ref({ ...todo }) +} +``` + +### Use stable members in deps + +```tsx +import * as React from 'react' +import { useQuery } from '@tanstack/react-query' + +export function TodosEffect() { + const { data } = useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + }) + React.useEffect(() => console.log(data?.length ?? 0), [data]) + return null +} +``` + +## Common Mistakes + +### MEDIUM Rest destructuring + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodos() { + const { data, ...rest } = useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + }) + return { data, rest } +} +``` + +Correct: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodos() { + const { data, isFetching, error } = useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + }) + return { data, isFetching, error } +} +``` + +Rest destructuring touches every tracked property and disables fine-grained render tracking. + +Source: TanStack/query:docs/eslint/no-rest-destructuring.md + +### HIGH Query result in hook deps + +Wrong: + +```tsx +import * as React from 'react' +import { useQuery } from '@tanstack/react-query' + +export function Todos() { + const query = useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + }) + React.useEffect(() => console.log(query.data), [query]) + return null +} +``` + +Correct: + +```tsx +import * as React from 'react' +import { useQuery } from '@tanstack/react-query' + +export function Todos() { + const { data } = useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + }) + React.useEffect(() => console.log(data), [data]) + return null +} +``` + +The top-level query result object is not referentially stable; destructured members are tracked. + +Source: TanStack/query:docs/eslint/no-unstable-deps.md + +### HIGH Vue v-model mutates query result + +Wrong: + +```vue + + +``` + +Correct: + +```vue + + +``` + +Vue Query results are immutable; make a mutable copy for form state. + +Source: TanStack/query:docs/framework/vue/reactivity.md + +See also: `compositions/enforce-query-best-practices-with-eslint` for lint rules that protect render tracking. diff --git a/packages/query-intent/skills/framework/use-framework-adapter-reactivity/SKILL.md b/packages/query-intent/skills/framework/use-framework-adapter-reactivity/SKILL.md new file mode 100644 index 00000000000..2ee691daa7c --- /dev/null +++ b/packages/query-intent/skills/framework/use-framework-adapter-reactivity/SKILL.md @@ -0,0 +1,185 @@ +--- +name: framework/use-framework-adapter-reactivity +description: > + Use this when translating Query patterns across React, Preact, Vue, Solid, + Svelte, Angular, and Lit adapters: Vue refs/getters, Solid signals, Svelte + stores/runes, Angular signals and HttpClient promises, Lit QueryController, + adapter option helpers, and provider APIs. +type: framework +library: TanStack Query +framework: cross-adapter +library_version: '5.101.0' +requires: + - lifecycle/setup-query-client-and-providers + - core/design-query-keys-and-options +sources: + - TanStack/query:docs/framework/vue/reactivity.md + - TanStack/query:docs/framework/vue/guides/query-options.md + - TanStack/query:docs/framework/solid/guides/query-options.md + - TanStack/query:docs/framework/svelte/overview.md + - TanStack/query:docs/framework/svelte/migrate-from-v5-to-v6.md + - TanStack/query:docs/framework/angular/overview.md + - TanStack/query:docs/framework/angular/zoneless.md + - TanStack/query:docs/framework/lit/guides/reactive-controllers-vs-hooks.md +--- + +This skill builds on `lifecycle/setup-query-client-and-providers` and `core/design-query-keys-and-options`. + +## Setup + +```ts +import { queryOptions } from '@tanstack/react-query' + +export function todoOptions(id: string) { + return queryOptions({ + queryKey: ['todo', id], + queryFn: async () => ({ id }), + }) +} +``` + +## Hooks and Components + +### Keep Vue refs in the key + +```ts +import { computed, toRef } from 'vue' +import { useQuery } from '@tanstack/vue-query' + +export function useTodo(props: { id: string }) { + const id = toRef(props, 'id') + return useQuery( + computed(() => ({ + queryKey: ['todo', id.value], + queryFn: async () => ({ id: id.value }), + })), + ) +} +``` + +### Use Solid option functions + +```ts +import { createQuery } from '@tanstack/solid-query' + +export function useTodo(id: () => string) { + return createQuery(() => ({ + queryKey: ['todo', id()], + queryFn: async () => ({ id: id() }), + })) +} +``` + +### Convert Angular Observable clients to promises + +```ts +import { injectQuery } from '@tanstack/angular-query-experimental' +import { firstValueFrom, of } from 'rxjs' + +export class TodosQuery { + todos = injectQuery(() => ({ + queryKey: ['todos'], + queryFn: () => firstValueFrom(of([{ id: 1 }])), + })) +} +``` + +## Common Mistakes + +### HIGH Vue ref unwrapped + +Wrong: + +```ts +import { useQuery } from '@tanstack/vue-query' + +export function useTodo(id: { value: string }) { + return useQuery({ + queryKey: ['todo', id.value], + queryFn: async () => ({ id: id.value }), + }) +} +``` + +Correct: + +```ts +import { computed } from 'vue' +import { useQuery } from '@tanstack/vue-query' + +export function useTodo(id: { value: string }) { + return useQuery( + computed(() => ({ + queryKey: ['todo', id.value], + queryFn: async () => ({ id: id.value }), + })), + ) +} +``` + +Vue reactive inputs need to stay reactive through the options object or query key. + +Source: TanStack/query:docs/framework/vue/reactivity.md + +### HIGH Svelte store syntax in v6 + +Wrong: + +```svelte + +``` + +Correct: + +```svelte + +``` + +Svelte Query v6 uses rune-compatible option functions rather than the older store shape. + +Source: TanStack/query:docs/framework/svelte/migrate-from-v5-to-v6.md + +### HIGH Angular Observable returned directly + +Wrong: + +```ts +import { injectQuery } from '@tanstack/angular-query-experimental' +import { of } from 'rxjs' + +export class TodosQuery { + todos = injectQuery(() => ({ + queryKey: ['todos'], + queryFn: () => of([{ id: 1 }]), + })) +} +``` + +Correct: + +```ts +import { injectQuery } from '@tanstack/angular-query-experimental' +import { firstValueFrom, of } from 'rxjs' + +export class TodosQuery { + todos = injectQuery(() => ({ + queryKey: ['todos'], + queryFn: () => firstValueFrom(of([{ id: 1 }])), + })) +} +``` + +Query functions must resolve data; Angular HttpClient Observables need conversion to promises. + +Source: TanStack/query:docs/framework/angular/angular-httpclient-and-other-data-fetching-clients.md + +See also: `core/design-query-keys-and-options` for key identity across adapters. diff --git a/packages/query-intent/skills/framework/use-suspense-and-error-boundaries/SKILL.md b/packages/query-intent/skills/framework/use-suspense-and-error-boundaries/SKILL.md new file mode 100644 index 00000000000..12b9de00dd3 --- /dev/null +++ b/packages/query-intent/skills/framework/use-suspense-and-error-boundaries/SKILL.md @@ -0,0 +1,210 @@ +--- +name: framework/use-suspense-and-error-boundaries +description: > + Use this when using useSuspenseQuery, useSuspenseQueries, + useSuspenseInfiniteQuery, QueryErrorResetBoundary, throwOnError, + useQueryErrorResetBoundary, React.use query.promise, streamed hydration, and + Suspense constraints in Query adapters. +type: framework +library: TanStack Query +framework: cross-adapter +library_version: '5.101.0' +requires: + - core/fetch-and-observe-queries + - lifecycle/prefetch-and-remove-request-waterfalls +sources: + - TanStack/query:docs/framework/react/guides/suspense.md + - TanStack/query:docs/framework/react/reference/QueryErrorResetBoundary.md + - TanStack/query:docs/framework/react/reference/useSuspenseQuery.md + - TanStack/query:docs/framework/react/reference/useSuspenseQueries.md + - TanStack/query:docs/framework/react/reference/useSuspenseInfiniteQuery.md + - TanStack/query:docs/framework/solid/guides/suspense.md + - TanStack/query:docs/framework/vue/guides/suspense.md +--- + +This skill builds on `core/fetch-and-observe-queries` and `lifecycle/prefetch-and-remove-request-waterfalls`. + +## Setup + +```tsx +import { Suspense } from 'react' +import { useSuspenseQuery } from '@tanstack/react-query' + +function Todos() { + const { data } = useSuspenseQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + }) + return
    {JSON.stringify(data)}
    +} + +export function App() { + return ( + Loading

    }> + +
    + ) +} +``` + +## Hooks and Components + +### Reset query errors with the boundary + +```tsx +import { QueryErrorResetBoundary } from '@tanstack/react-query' +import { ErrorBoundary } from 'react-error-boundary' + +export function QueryErrorBoundary(props: { children: React.ReactNode }) { + return ( + + {({ reset }) => ( + ( + + )} + > + {props.children} + + )} + + ) +} +``` + +### Use suspense when data must be defined + +```ts +import { useSuspenseQuery } from '@tanstack/react-query' + +export function useTodos() { + return useSuspenseQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + }) +} +``` + +### Use normal queries for disabled flows + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useMaybeTodo(id: string | undefined) { + return useQuery({ + queryKey: ['todo', id], + queryFn: async () => ({ id }), + enabled: Boolean(id), + }) +} +``` + +## Common Mistakes + +### HIGH Disabled suspense query + +Wrong: + +```ts +import { useSuspenseQuery } from '@tanstack/react-query' + +export function useTodo(id: string | undefined) { + return useSuspenseQuery({ + queryKey: ['todo', id], + queryFn: async () => ({ id }), + enabled: Boolean(id), + }) +} +``` + +Correct: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodo(id: string | undefined) { + return useQuery({ + queryKey: ['todo', id], + queryFn: async () => ({ id }), + enabled: Boolean(id), + }) +} +``` + +Suspense hooks guarantee `data` and do not support conditional disabling like normal queries. + +Source: TanStack/query:docs/framework/react/guides/suspense.md + +### HIGH Missing reset boundary + +Wrong: + +```tsx +import { useSuspenseQuery } from '@tanstack/react-query' + +export function Todos() { + const { data } = useSuspenseQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + }) + return
    {JSON.stringify(data)}
    +} +``` + +Correct: + +```tsx +import { QueryErrorResetBoundary } from '@tanstack/react-query' +import { ErrorBoundary } from 'react-error-boundary' + +export function WrappedTodos(props: { children: React.ReactNode }) { + return ( + + {({ reset }) => ( + Error

    }> + {props.children} +
    + )} +
    + ) +} +``` + +Error boundaries need Query reset coordination so failed queries can retry after reset. + +Source: TanStack/query:docs/framework/react/reference/QueryErrorResetBoundary.md + +### MEDIUM query.promise without flag + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodoPromise() { + return useQuery({ queryKey: ['todo', 1], queryFn: async () => ({ id: 1 }) }) + .promise +} +``` + +Correct: + +```ts +import { QueryClient, useQuery } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + defaultOptions: { queries: { experimental_prefetchInRender: true } }, +}) + +export function useTodoPromise() { + return useQuery({ queryKey: ['todo', 1], queryFn: async () => ({ id: 1 }) }) + .promise +} +``` + +The stable `promise` property requires the experimental prefetch-in-render flag. + +Source: TanStack/query:docs/framework/react/guides/suspense.md + +See also: `lifecycle/ssr-hydration-and-streaming` for streamed hydration constraints. diff --git a/packages/query-intent/skills/lifecycle/migrate-major-versions-and-codemods/SKILL.md b/packages/query-intent/skills/lifecycle/migrate-major-versions-and-codemods/SKILL.md new file mode 100644 index 00000000000..5c6b04a5b8b --- /dev/null +++ b/packages/query-intent/skills/lifecycle/migrate-major-versions-and-codemods/SKILL.md @@ -0,0 +1,169 @@ +--- +name: lifecycle/migrate-major-versions-and-codemods +description: > + Use this when migrating TanStack Query across React Query v3, React Query v4, + TanStack Query v5, Vue Query v5, Svelte Query v6, removed overloads, + object syntax, cacheTime to gcTime, removed query callbacks, keepPreviousData + migration, and query-codemods. +type: lifecycle +library: TanStack Query +library_version: '5.101.0' +sources: + - TanStack/query:docs/framework/react/guides/migrating-to-react-query-3.md + - TanStack/query:docs/framework/react/guides/migrating-to-react-query-4.md + - TanStack/query:docs/framework/react/guides/migrating-to-v5.md + - TanStack/query:docs/framework/vue/guides/migrating-to-v5.md + - TanStack/query:docs/framework/svelte/migrate-from-v5-to-v6.md + - TanStack/query:packages/query-codemods/package.json +--- + +## Setup + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodos() { + return useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + gcTime: 5 * 60 * 1000, + }) +} +``` + +## Core Patterns + +### Use object syntax everywhere + +```ts +import { QueryClient, useQuery } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +export function useTodo(id: string) { + return useQuery({ queryKey: ['todo', id], queryFn: async () => ({ id }) }) +} + +export function prefetchTodo(id: string) { + return queryClient.prefetchQuery({ + queryKey: ['todo', id], + queryFn: async () => ({ id }), + }) +} +``` + +### Rename cacheTime to gcTime + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + defaultOptions: { queries: { gcTime: 10 * 60 * 1000 } }, +}) +``` + +### Migrate keepPreviousData + +```ts +import { keepPreviousData, useQuery } from '@tanstack/react-query' + +export function usePage(page: number) { + return useQuery({ + queryKey: ['page', page], + queryFn: async () => ({ page }), + placeholderData: keepPreviousData, + }) +} +``` + +## Common Mistakes + +### CRITICAL v4 overload syntax + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodos() { + return useQuery(['todos'], async () => [{ id: 1 }]) +} +``` + +Correct: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodos() { + return useQuery({ queryKey: ['todos'], queryFn: async () => [{ id: 1 }] }) +} +``` + +v5 removed hook and client overloads in favor of a single object signature. + +Source: TanStack/query:docs/framework/react/guides/migrating-to-v5.md + +### HIGH Removed query callbacks + +Wrong: + +```ts +import { useQuery } from '@tanstack/react-query' + +export function useTodos() { + return useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + onSuccess: () => console.log('loaded'), + }) +} +``` + +Correct: + +```tsx +import * as React from 'react' +import { useQuery } from '@tanstack/react-query' + +export function TodosLogger() { + const { data } = useQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + }) + React.useEffect(() => { + if (data) console.log('loaded') + }, [data]) + return null +} +``` + +v5 removed query callbacks from queries; react to data changes outside the query options. + +Source: TanStack/query:docs/framework/react/guides/migrating-to-v5.md + +### HIGH cacheTime in v5 + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + defaultOptions: { queries: { cacheTime: 60_000 } }, +}) +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + defaultOptions: { queries: { gcTime: 60_000 } }, +}) +``` + +`cacheTime` was renamed to `gcTime` to describe garbage collection of unused queries. + +Source: TanStack/query:docs/framework/react/guides/migrating-to-v5.md diff --git a/packages/query-intent/skills/lifecycle/prefetch-and-remove-request-waterfalls/SKILL.md b/packages/query-intent/skills/lifecycle/prefetch-and-remove-request-waterfalls/SKILL.md new file mode 100644 index 00000000000..f4429b916b9 --- /dev/null +++ b/packages/query-intent/skills/lifecycle/prefetch-and-remove-request-waterfalls/SKILL.md @@ -0,0 +1,228 @@ +--- +name: lifecycle/prefetch-and-remove-request-waterfalls +description: > + Use this when flattening request waterfalls with prefetchQuery, + prefetchInfiniteQuery, ensureQueryData, fetchQuery, usePrefetchQuery, + usePrefetchInfiniteQuery, route loaders, TanStack Router loaders, Start routes, + Suspense prefetching, and query-function prefetching. +type: lifecycle +library: TanStack Query +library_version: '5.101.0' +requires: + - core/design-query-keys-and-options + - core/fetch-and-observe-queries + - core/tune-defaults-freshness-retries-and-refetching +sources: + - TanStack/query:docs/framework/react/guides/prefetching.md + - TanStack/query:docs/framework/react/guides/request-waterfalls.md + - TanStack/query:docs/framework/react/reference/usePrefetchQuery.md + - TanStack/query:docs/framework/react/reference/usePrefetchInfiniteQuery.md + - TanStack/query:docs/reference/QueryClient.md + - TanStack/query:examples/react/react-router/src/routes/root.tsx + - TanStack/query:examples/react/react-router/src/routes/contact.tsx +--- + +## Setup + +```ts +import { QueryClient, queryOptions } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +const todosOptions = queryOptions({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1, title: 'Ship' }], + staleTime: 60_000, +}) + +export function preloadTodos() { + return queryClient.prefetchQuery(todosOptions) +} +``` + +## Core Patterns + +### Use loaders for route-critical data + +```ts +import { QueryClient, queryOptions } from '@tanstack/react-query' + +const queryClient = new QueryClient() +const contactOptions = (contactId: string) => + queryOptions({ + queryKey: ['contact', contactId], + queryFn: async () => ({ id: contactId, name: 'Ada' }), + }) + +export function contactLoader(contactId: string) { + return queryClient.ensureQueryData(contactOptions(contactId)) +} +``` + +### Prefetch before Suspense can suspend + +```tsx +import { Suspense } from 'react' +import { usePrefetchQuery, useSuspenseQuery } from '@tanstack/react-query' + +const commentsOptions = { + queryKey: ['comments'], + queryFn: async () => [{ id: 1 }], +} + +function Comments() { + const { data } = useSuspenseQuery(commentsOptions) + return
    {JSON.stringify(data)}
    +} + +export function CommentsSection() { + usePrefetchQuery(commentsOptions) + return ( + Loading

    }> + +
    + ) +} +``` + +### Pick the right client method + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +export function getTodo(id: string) { + return queryClient.fetchQuery({ + queryKey: ['todo', id], + queryFn: async () => ({ id }), + }) +} +``` + +Use `fetchQuery` when the caller needs data or thrown errors; use `prefetchQuery` when it only needs to warm the cache. + +## Common Mistakes + +### HIGH Expecting prefetchQuery to return data + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() +const data = await queryClient.prefetchQuery({ + queryKey: ['todo', 1], + queryFn: async () => ({ id: 1 }), +}) +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() +const data = await queryClient.fetchQuery({ + queryKey: ['todo', 1], + queryFn: async () => ({ id: 1 }), +}) +``` + +`prefetchQuery` returns void and swallows errors; `fetchQuery` returns data and throws. + +Source: TanStack/query:docs/framework/react/guides/prefetching.md + +### HIGH Prefetch staleTime only set on prefetch + +Wrong: + +```ts +import { QueryClient, useQuery } from '@tanstack/react-query' + +const queryClient = new QueryClient() +await queryClient.prefetchQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + staleTime: 60_000, +}) +export function useTodos() { + return useQuery({ queryKey: ['todos'], queryFn: async () => [{ id: 1 }] }) +} +``` + +Correct: + +```ts +import { QueryClient, queryOptions, useQuery } from '@tanstack/react-query' + +const queryClient = new QueryClient() +const options = queryOptions({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + staleTime: 60_000, +}) +await queryClient.prefetchQuery(options) +export function useTodos() { + return useQuery(options) +} +``` + +Prefetch call options do not automatically configure the later observer. + +Source: TanStack/query:docs/framework/react/guides/prefetching.md + +### HIGH Suspense prefetch after suspension + +Wrong: + +```tsx +import * as React from 'react' +import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query' + +export function Article() { + const queryClient = useQueryClient() + const article = useSuspenseQuery({ + queryKey: ['article'], + queryFn: async () => ({ id: 1 }), + }) + React.useEffect(() => { + queryClient.prefetchQuery({ + queryKey: ['comments'], + queryFn: async () => [], + }) + }, [queryClient]) + return
    {JSON.stringify(article.data)}
    +} +``` + +Correct: + +```tsx +import { Suspense } from 'react' +import { usePrefetchQuery, useSuspenseQuery } from '@tanstack/react-query' + +function Article() { + const article = useSuspenseQuery({ + queryKey: ['article'], + queryFn: async () => ({ id: 1 }), + }) + return
    {JSON.stringify(article.data)}
    +} + +export function ArticleRoute() { + usePrefetchQuery({ queryKey: ['comments'], queryFn: async () => [] }) + return ( + Loading

    }> +
    + + ) +} +``` + +Effects do not run until after a suspenseful query resolves, so they cannot flatten that waterfall. + +Source: TanStack/query:docs/framework/react/guides/prefetching.md + +See also: `compositions/compose-query-with-tanstack-router-and-start` for Router and Start loader prefetching. diff --git a/packages/query-intent/skills/lifecycle/setup-query-client-and-providers/SKILL.md b/packages/query-intent/skills/lifecycle/setup-query-client-and-providers/SKILL.md new file mode 100644 index 00000000000..62132cf734b --- /dev/null +++ b/packages/query-intent/skills/lifecycle/setup-query-client-and-providers/SKILL.md @@ -0,0 +1,177 @@ +--- +name: lifecycle/setup-query-client-and-providers +description: > + Use this when creating a TanStack Query QueryClient, QueryClientProvider, VueQueryPlugin, + provideTanStackQuery, Svelte QueryClientProvider, Angular providers, Lit controllers, or + SSR request-local clients. Covers stable client lifetime and provider wiring. +type: lifecycle +library: TanStack Query +library_version: '5.101.0' +sources: + - TanStack/query:docs/framework/react/quick-start.md + - TanStack/query:docs/framework/react/reference/QueryClientProvider.md + - TanStack/query:docs/eslint/stable-query-client.md + - TanStack/query:docs/framework/vue/guides/custom-client.md + - TanStack/query:docs/framework/angular/overview.md + - TanStack/query:docs/framework/lit/guides/reactive-controllers-vs-hooks.md +--- + +## Setup + +```tsx +import * as React from 'react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +export function AppProviders(props: { children: React.ReactNode }) { + const [queryClient] = React.useState(() => new QueryClient()) + return ( + + {props.children} + + ) +} +``` + +## Core Patterns + +### Create request-local SSR clients + +```ts +import { QueryClient } from '@tanstack/react-query' + +export function createServerQueryClient() { + return new QueryClient({ + defaultOptions: { queries: { staleTime: 60_000 } }, + }) +} +``` + +### Use adapter-native providers + +```ts +import { QueryClient } from '@tanstack/query-core' + +export function createQueryClient() { + return new QueryClient({ + defaultOptions: { queries: { retry: 2 } }, + }) +} +``` + +React and Preact use `QueryClientProvider`; Vue uses `VueQueryPlugin`; Angular uses `provideTanStackQuery`; Svelte and Lit use their adapter provider/controller APIs. + +### Pass an explicit client at integration boundaries + +```ts +import { QueryClient } from '@tanstack/query-core' + +const queryClient = new QueryClient() + +export function getTodos() { + return queryClient.ensureQueryData({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1, title: 'Ship' }], + }) +} +``` + +## Common Mistakes + +### CRITICAL New client on every render + +Wrong: + +```tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +export function App(props: { children: React.ReactNode }) { + const queryClient = new QueryClient() + return ( + + {props.children} + + ) +} +``` + +Correct: + +```tsx +import * as React from 'react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +export function App(props: { children: React.ReactNode }) { + const [queryClient] = React.useState(() => new QueryClient()) + return ( + + {props.children} + + ) +} +``` + +Recreating the client discards caches and subscriptions on render. + +Source: TanStack/query:docs/eslint/stable-query-client.md + +### CRITICAL Shared SSR cache between users + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient() +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export function createRequestQueryClient() { + return new QueryClient() +} +``` + +A module-level server client can leak one request's cached data into another request. + +Source: TanStack/query:docs/framework/react/guides/ssr.md + +### HIGH Ambiguous Lit fallback client + +Wrong: + +```ts +import { QueryController } from '@tanstack/lit-query' + +export class TodoElement extends HTMLElement { + todos = new QueryController(this, { + queryKey: ['todos'], + queryFn: async () => [], + }) +} +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/query-core' +import { QueryController } from '@tanstack/lit-query' + +const queryClient = new QueryClient() + +export class TodoElement extends HTMLElement { + todos = new QueryController( + this, + { queryKey: ['todos'], queryFn: async () => [] }, + queryClient, + ) +} +``` + +Lit controllers need a clear provider or explicit client when the element is not under a provider tree. + +Source: TanStack/query:docs/framework/lit/guides/reactive-controllers-vs-hooks.md + +See also: `lifecycle/ssr-hydration-and-streaming` for server cache handoff. diff --git a/packages/query-intent/skills/lifecycle/ssr-hydration-and-streaming/SKILL.md b/packages/query-intent/skills/lifecycle/ssr-hydration-and-streaming/SKILL.md new file mode 100644 index 00000000000..5e5adac7b48 --- /dev/null +++ b/packages/query-intent/skills/lifecycle/ssr-hydration-and-streaming/SKILL.md @@ -0,0 +1,229 @@ +--- +name: lifecycle/ssr-hydration-and-streaming +description: > + Use this when implementing SSR, hydration, dehydrate, hydrate, + HydrationBoundary, TanStack Start, TanStack Router SSR Query, Next.js app + router, Next.js pages router, React Server Components, SvelteKit, Nuxt, + SolidStart, Lit SSR, ReactQueryStreamedHydration, or streamedQuery. +type: lifecycle +library: TanStack Query +library_version: '5.101.0' +requires: + - lifecycle/setup-query-client-and-providers + - lifecycle/prefetch-and-remove-request-waterfalls +sources: + - TanStack/query:docs/framework/react/guides/ssr.md + - TanStack/query:docs/framework/react/guides/advanced-ssr.md + - TanStack/query:docs/framework/react/reference/hydration.md + - TanStack/query:docs/reference/environmentManager.md + - TanStack/query:docs/reference/streamedQuery.md + - TanStack/query:docs/framework/vue/guides/ssr.md + - TanStack/query:docs/framework/svelte/ssr.md + - TanStack/query:docs/framework/solid/guides/ssr.md + - TanStack/query:docs/framework/lit/guides/ssr.md +--- + +## Setup + +```tsx +import { + HydrationBoundary, + QueryClient, + dehydrate, +} from '@tanstack/react-query' + +export async function PostsPage() { + const queryClient = new QueryClient() + await queryClient.prefetchQuery({ + queryKey: ['posts'], + queryFn: async () => [{ id: 1 }], + }) + return ( + + + + ) +} + +function Posts() { + return

    Hydrated posts render here

    +} +``` + +## Core Patterns + +### Put TanStack Start and Router first + +```ts +import { QueryClient } from '@tanstack/react-query' +import { createRouter } from '@tanstack/react-router' +import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query' +import { routeTree } from './routeTree.gen' + +const queryClient = new QueryClient() +const router = createRouter({ routeTree, context: { queryClient } }) + +setupRouterSsrQueryIntegration({ router, queryClient }) +``` + +### Use per-request clients + +```ts +import { QueryClient } from '@tanstack/react-query' + +export function createSsrQueryClient() { + return new QueryClient({ + defaultOptions: { queries: { staleTime: 60_000 } }, + }) +} +``` + +### Hydrate only prefetched data + +```tsx +import { + HydrationBoundary, + QueryClient, + dehydrate, +} from '@tanstack/react-query' + +export async function Page() { + const queryClient = new QueryClient() + await queryClient.prefetchQuery({ + queryKey: ['profile'], + queryFn: async () => ({ name: 'Tanner' }), + }) + return ( + +
    Profile
    +
    + ) +} +``` + +## Common Mistakes + +### CRITICAL RSC renders fetched data twice + +Wrong: + +```tsx +import { + HydrationBoundary, + QueryClient, + dehydrate, +} from '@tanstack/react-query' + +export async function Page() { + const queryClient = new QueryClient() + const posts = await queryClient.fetchQuery({ + queryKey: ['posts'], + queryFn: async () => [{ id: 1 }], + }) + return ( + <> +

    {posts.length}

    + +
    Posts
    +
    + + ) +} +``` + +Correct: + +```tsx +import { + HydrationBoundary, + QueryClient, + dehydrate, +} from '@tanstack/react-query' + +export async function Page() { + const queryClient = new QueryClient() + await queryClient.prefetchQuery({ + queryKey: ['posts'], + queryFn: async () => [{ id: 1 }], + }) + return ( + +
    Posts
    +
    + ) +} +``` + +Server-rendered derived data can desynchronize from client-refetched Query data. + +Source: TanStack/query:docs/framework/react/guides/advanced-ssr.md + +### CRITICAL Suspense query not prefetched on server + +Wrong: + +```tsx +import { useSuspenseQuery } from '@tanstack/react-query' + +export function Posts() { + const { data } = useSuspenseQuery({ + queryKey: ['posts'], + queryFn: async () => [{ id: 1 }], + }) + return
    {JSON.stringify(data)}
    +} +``` + +Correct: + +```tsx +import { + HydrationBoundary, + QueryClient, + dehydrate, +} from '@tanstack/react-query' + +export async function Page() { + const queryClient = new QueryClient() + await queryClient.prefetchQuery({ + queryKey: ['posts'], + queryFn: async () => [{ id: 1 }], + }) + return ( + +
    Posts
    +
    + ) +} +``` + +A suspense query that is not prefetched can fetch on the server, fail to hydrate, then fetch again on the client. + +Source: TanStack/query:docs/framework/react/guides/ssr.md + +### HIGH SvelteKit query runs after SSR response + +Wrong: + +```ts +import { QueryClient } from '@tanstack/svelte-query' + +export const queryClient = new QueryClient() +``` + +Correct: + +```ts +import { browser } from '$app/environment' +import { QueryClient } from '@tanstack/svelte-query' + +export const queryClient = new QueryClient({ + defaultOptions: { queries: { enabled: browser } }, +}) +``` + +SvelteKit SSR needs browser-gated default query execution unless server data is explicitly prefetched. + +Source: TanStack/query:docs/framework/svelte/ssr.md + +See also: `compositions/compose-query-with-tanstack-router-and-start` for TanStack-owned SSR routing. diff --git a/packages/query-intent/skills/lifecycle/test-query-code/SKILL.md b/packages/query-intent/skills/lifecycle/test-query-code/SKILL.md new file mode 100644 index 00000000000..3c686c055f6 --- /dev/null +++ b/packages/query-intent/skills/lifecycle/test-query-code/SKILL.md @@ -0,0 +1,164 @@ +--- +name: lifecycle/test-query-code +description: > + Use this when testing TanStack Query code with isolated QueryClient instances, + test providers, retry false, cache cleanup, async assertions, hook testing, + React/Vue/Solid/Angular harnesses, and deterministic query state. +type: lifecycle +library: TanStack Query +library_version: '5.101.0' +requires: + - lifecycle/setup-query-client-and-providers + - core/tune-defaults-freshness-retries-and-refetching +sources: + - TanStack/query:docs/framework/react/guides/testing.md + - TanStack/query:docs/framework/solid/guides/testing.md + - TanStack/query:docs/framework/vue/guides/testing.md + - TanStack/query:docs/framework/angular/guides/testing.md + - TanStack/query:packages/query-core/src/__tests__/queryClient.test.tsx + - TanStack/query:packages/react-query/src/__tests__/useQuery.test.tsx +--- + +## Setup + +```tsx +import * as React from 'react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +export function createTestWrapper() { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }) + + return function TestWrapper(props: { children: React.ReactNode }) { + return ( + + {props.children} + + ) + } +} +``` + +## Core Patterns + +### Create a client per test + +```ts +import { QueryClient } from '@tanstack/react-query' + +export function createTestQueryClient() { + return new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }) +} +``` + +### Await async state + +```ts +import { QueryClient } from '@tanstack/react-query' + +export async function loadTodosForTest() { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }) + return queryClient.fetchQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], + }) +} +``` + +### Clear after direct client tests + +```ts +import { QueryClient } from '@tanstack/react-query' + +export function dispose(queryClient: QueryClient) { + queryClient.clear() +} +``` + +## Common Mistakes + +### CRITICAL Shared test client + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient() +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export function createQueryClient() { + return new QueryClient({ defaultOptions: { queries: { retry: false } } }) +} +``` + +A shared client leaks cache and mutation state across tests. + +Source: TanStack/query:docs/framework/react/guides/testing.md + +### HIGH Retry backoff in tests + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient() +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, +}) +``` + +Default retries delay failure and make tests look hung. + +Source: TanStack/query:docs/framework/react/guides/testing.md + +### HIGH Asserting before async success + +Wrong: + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() +const promise = queryClient.fetchQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], +}) +console.log(queryClient.getQueryData(['todos'])) +await promise +``` + +Correct: + +```ts +import { QueryClient } from '@tanstack/react-query' + +const queryClient = new QueryClient() +await queryClient.fetchQuery({ + queryKey: ['todos'], + queryFn: async () => [{ id: 1 }], +}) +console.log(queryClient.getQueryData(['todos'])) +``` + +Query state is asynchronous; assert after the query promise resolves or the UI wait completes. + +Source: TanStack/query:docs/framework/react/guides/testing.md diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 859c42dd7a9..c1f750a5057 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,6 +49,12 @@ importers: '@tanstack/eslint-config': specifier: 0.3.2 version: 0.3.2(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@tanstack/intent': + specifier: ^0.0.41 + version: 0.0.41 + '@tanstack/query-intent': + specifier: workspace:* + version: link:packages/query-intent '@tanstack/typedoc-config': specifier: 0.3.1 version: 0.3.1(typescript@5.9.3) @@ -2765,6 +2771,12 @@ importers: specifier: ^2.11.6 version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + packages/query-intent: + devDependencies: + '@tanstack/intent': + specifier: ^0.0.41 + version: 0.0.41 + packages/query-persist-client-core: dependencies: '@tanstack/query-core': @@ -7288,6 +7300,10 @@ packages: resolution: {integrity: sha512-2g+PuGR3GuvvCiR3xZs+IMqAvnYU9bvH+jRml0BFBSxHBj22xFCTNvJWhvgj7uICFF9IchDkFUto91xDPMu5cg==} engines: {node: '>=18'} + '@tanstack/intent@0.0.41': + resolution: {integrity: sha512-Psl+oDiidLvtctswkTQ1P6sQIihwrMLcdfQVfkLpO42oKwxWEr1lodWUHiOG5jFXsGwDDvpUv/WAdlmJF+yGpw==} + hasBin: true + '@tanstack/match-sorter-utils@8.19.4': resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} engines: {node: '>=12'} @@ -21160,6 +21176,13 @@ snapshots: - supports-color - typescript + '@tanstack/intent@0.0.41': + dependencies: + cac: 6.7.14 + jsonc-parser: 3.3.1 + semver: 7.7.4 + yaml: 2.8.3 + '@tanstack/match-sorter-utils@8.19.4': dependencies: remove-accents: 0.5.0