tanstack-query

Use TanStack Query for data fetching and caching. Use when implementing queries, mutations, infinite queries, or cache invalidation.

oakoss/open-saas-kit1 installsMITSynced Aug 26

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: tanstack-query
description: Use TanStack Query for data fetching and caching. Use when implementing queries, mutations, infinite queries, or cache invalidation.
license: MIT
---

# TanStack Query

## Mental Model

TanStack Query is an **async state manager**, NOT a data fetching library. It doesn't fetch data - you provide a `queryFn` that returns a Promise. React Query handles caching, deduplication, background updates, and stale data management.

**Key distinctions:**

| Concept      | Client State           | Server State (React Query) |
| ------------ | ---------------------- | -------------------------- |
| Ownership    | You control completely | Persisted remotely         |
| Availability | Synchronous            | Asynchronous               |
| Updates      | Predictable            | Can become outdated        |
| Management   | useState/Zustand       | TanStack Query             |

**Query keys = dependency array:** Parameters used in your `queryFn` must appear in the `queryKey`. This ensures automatic refetches when dependencies change and prevents stale closure bugs.

## Basic Query

```tsx
import { useQuery } from '@tanstack/react-query';

const { data, error, isPending, isFetching } = useQuery({
  queryKey: ['posts'],
  queryFn: async () => {
    const res = await fetch('/api/posts');
    if (!res.ok) throw new Error('Failed to fetch');
    return res.json();
  },
  staleTime: 1000 * 60, // Fresh for 1 minute
  gcTime: 1000 * 60 * 5, // Cache for 5 minutes
  select: (data) => data.slice(0, 10), // Transform data (optional)
});

// Data-first rendering pattern
if (data) return <PostList posts={data} />;
if (error) return <Error message={error.message} />;
return <Skeleton />;
```

## Query Options Helper

```tsx
import { queryOptions, useQuery, useQueryClient } from '@tanstack/react-query';

function postOptions(id: string) {
  return queryOptions({
    queryKey: ['posts', id],
    queryFn: () => fetchPost(id),
    staleTime: 1000 * 60,
  });
}

// Usage - same options everywhere
useQuery(postOptions('123'));
useSuspenseQuery(postOptions('456'));
queryClient.prefetchQuery(postOptions('789'));
queryClient.invalidateQueries({ queryKey: postOptions('123').queryKey });
```

## Query Key Factory (TkDodo Pattern)

For granular cache invalidation, separate keys from options:

```tsx
// apps/web/src/modules/posts/hooks/post-queries.ts
export const postKeys = {
  all: ['posts'] as const,
  lists: () => [...postKeys.all, 'list'] as const,
  list: (filters: PostFilters) => [...postKeys.lists(), filters] as const,
  details: () => [...postKeys.all, 'detail'] as const,
  detail: (id: string) => [...postKeys.details(), id] as const,
};

export const postQueries = {
  list: (filters: PostFilters) =>
    queryOptions({
      queryKey: postKeys.list(filters),
      queryFn: () => getPosts({ data: filters }),
    }),
  detail: (id: string) =>
    queryOptions({
      queryKey: postKeys.detail(id),
      queryFn: () => getPost({ data: { id } }),
    }),
};

// Granular invalidation
queryClient.invalidateQueries({ queryKey: postKeys.all }); // All posts
queryClient.invalidateQueries({ queryKey: postKeys.lists() }); // All lists
queryClient.invalidateQueries({ queryKey: postKeys.detail('123') }); // One post
```

See [Query Patterns](query-patterns.md) for more key factory patterns.

## Mutations

```tsx
const queryClient = useQueryClient();

const mutation = useMutation({
  mutationFn: async (newPost: { title: string; body: string }) => {
    const res = await fetch('/api/posts', {
      method: 'POST',
      body: JSON.stringify(newPost),
    });
    return res.json();
  },
  onSuccess: async () => {
    await queryClient.invalidateQueries({ queryKey: ['posts'] });
  },
});

mutation.mutate(data);
// Or: await mutation.mutateAsync(data);
```

## Query with Server Functions

```tsx
import { createServerFn } from '@tanstack/react-start';

const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
  return await db.query.posts.findMany();
});

function postsOptions() {
  return queryOptions({
    queryKey: ['posts'],
    queryFn: () => getPosts(),
  });
}

// Prefetch in loader
export const Route = createFileRoute('/posts')({
  loader: async ({ context }) => {
    await context.queryClient.ensureQueryData(postsOptions());
  },
});
```

## Cache Operations

```tsx
const queryClient = useQueryClient();

// Invalidate
queryClient.invalidateQueries({ queryKey: ['posts'] });
queryClient.invalidateQueries({ queryKey: ['posts', '123'] });

// Set data directly
queryClient.setQueryData(['posts', '123'], newPost);

// Prefetch
await queryClient.prefetchQuery(postOptions('456'));
```

## State Comparison

| State        | Meaning                                  |
| ------------ | ---------------------------------------- |
| `isPending`  | No data yet (first load or disabled)     |
| `isLoading`  | First load, fetching, no cached data     |
| `isFetching` | Any fetch (including background refetch) |
| `isSuccess`  | Query succeeded, data available          |
| `isError`    | Query failed                             |

## Common Options

| Option                 | Default | Description                         |
| ---------------------- | ------- | ----------------------------------- |
| `staleTime`            | `0`     | Time until data is considered stale |
| `gcTime`               | 5 min   | Time to keep unused data in cache   |
| `retry`                | `3`     | Number of retry attempts            |
| `refetchOnWindowFocus` | `true`  | Refetch when window regains focus   |
| `enabled`              | `true`  | Whether query should execute        |

## Forms Integration (Brief)

Two approaches to combine forms with server state:

**Copy to Form State (Simple):**

```tsx
const { data } = useQuery(userOptions(id));
const [name, setName] = useState(data?.name ?? '');
// Form now independent of server state - loses background updates
```

**Derived State (Advanced):**

```tsx
const { data } = useQuery(userOptions(id));
const [nameOverride, setNameOverride] = useState<string>();
// Show user's input if changed, otherwise server value
<input
  value={nameOverride ?? data?.name}
  onChange={(e) => setNameOverride(e.target.value)}
/>;
```

See [tanstack-form skill](../tanstack-form/SKILL.md) for full form patterns.

## Common Mistakes

| Mistake                                        | Correct Pattern                                           |
| ---------------------------------------------- | --------------------------------------------------------- |
| Checking `isPending` before `data`             | Data-first: check `data` → `error` → `isPending`          |
| Copying server state to local useState         | Use data directly or derived state pattern                |
| Creating QueryClient in component              | Create once outside component or in useState              |
| Using `refetch()` for parameter changes        | Include params in queryKey, let it refetch automatically  |
| Same key for useQuery and useInfiniteQuery     | Use distinct key segments (different cache structures)    |
| Inline select without memoization              | Extract to stable function or useCallback                 |
| Using `catch` without re-throwing              | Throw errors in queryFn (fetch doesn't reject on 4xx/5xx) |
| Manual generics on useQuery                    | Type the queryFn return, let inference work               |
| Destructuring query for type narrowing         | Keep query object intact for proper narrowing             |
| Relying on deprecated onSuccess for state sync | Use the data directly from useQuery                       |
| Premature render optimization                  | Focus on correctness first, optimize later                |

## Delegation

- **Query pattern discovery**: For finding existing query implementations, use `Explore` agent
- **Cache strategy review**: For comprehensive cache analysis, use `Task` agent
- **Code review**: After implementing queries, delegate to `code-reviewer` agent

## Topic References

### Core Patterns

- [Query Patterns](query-patterns.md) - Key factories, dependent/parallel queries, context
- [Mutations](mutations.md) - Optimistic updates, auto-invalidation, callbacks
- [Caching Patterns](caching-patterns.md) - staleTime, gcTime, cache seeding, placeholderData
- [Infinite Queries](infinite-queries.md) - Cursor, offset, intersection observer

### Advanced Topics

- [Data Transformations](data-transformations.md) - select option, memoization, four approaches
- [Error Handling](error-handling.md) - Error boundaries, global handling, fetch errors
- [TypeScript Patterns](typescript-patterns.md) - Inference, skipToken, Zod validation
- [SSR Patterns](ssr-patterns.md) - Prefetching, hydration, React 19 Suspense
- [Testing Patterns](testing-patterns.md) - Isolation, MSW, async assertions
- [Advanced Patterns](advanced-patterns.md) - Offline, WebSockets, architecture

More Performance skills

seo-audit

coreyhaines31/marketingskills

When the user wants to audit, review, or diagnose SEO issues on their site. Also use when the user mentions "SEO audit," "technical SEO," "why am I not ranking," "SEO issues," "on-page SEO," "meta tags review," "SEO health check," "my traffic dropped," "lost rankings," "not showing up in Google," "site isn't ranking," "Google update hit me," "page speed," "core web vitals," "crawl errors," or "indexing issues." Use this even if the user just says something vague like "my SEO is bad" or "help with SEO" — start with an audit. For building pages at scale to target keywords, see programmatic-seo. For adding structured data, see schema. For AI search optimization, see ai-seo.

195.1k

competitor-profiling

coreyhaines31/marketingskills

When the user wants to research, profile, or analyze competitors from their URLs. Also use when the user mentions 'competitor profile,' 'competitor research,' 'competitor analysis,' 'profile this competitor,' 'analyze competitor,' 'competitive intelligence,' 'competitor deep dive,' 'who are my competitors,' 'competitor landscape,' 'competitor dossier,' 'competitive audit,' or 'research these competitors.' Input is a list of competitor URLs. Output is structured competitor profile markdown files. For creating comparison/alternative pages from profiles, see competitors. For sales-specific battle cards, see sales-enablement.

65.8k

prospecting

coreyhaines31/marketingskills

When the user wants to find, qualify, and build a list of prospects to reach out to — across B2B SaaS, general B2B, or local small businesses. Also use when the user mentions "prospecting," "build a prospect list," "find prospects," "find leads," "lead gen list," "find SaaS companies that," "find B2B companies," "find local businesses," "ICP-fit accounts," "who should we go after," "outbound list," "target account list," "find clients near me," "businesses without websites," "prospect research," "qualified leads," "find my first customers," "early adopters," "design partners," "beta users," or "who has this problem." Use this for the list-building and qualification phase. For writing the outbound copy after the list is built, see cold-email. For deep competitive research on specific accounts, see competitor-profiling.

42.1k

← All Performance skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY