一键导入
tanstack-query
Use TanStack Query for data fetching and caching. Use when implementing queries, mutations, infinite queries, or cache invalidation.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use TanStack Query for data fetching and caching. Use when implementing queries, mutations, infinite queries, or cache invalidation.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Git workflow for branches, commits, and PRs. Use for commit, branch, pr, pull request, conventional, push, feat, fix, chore, merge, rebase
Storybook stories and interaction tests. Use for story, stories, storybook, chromatic, visual test, interaction test, play function, component test
TypeScript and unicorn linting patterns. Use for typescript, array, for-of, reduce, forEach, throw, catch, modern js, es modules, string, number, error handling
Zod v4 schema validation. Use for zod, schema, validation, parse, safeParse, infer, coerce, transform, refine, z.object, z.string, z.email, z.url
Better Auth authentication. Use for auth, login, logout, session, user, signup, register, protect, middleware, password, oauth, social
Drizzle ORM + PostgreSQL database layer. Use for db, database, query, schema, table, migrate, sql, postgres, drizzle, model, relation
| name | tanstack-query |
| description | Use TanStack Query for data fetching and caching. Use when implementing queries, mutations, infinite queries, or cache invalidation. |
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.
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 />;
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 });
For granular cache invalidation, separate keys from options:
// 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 for more key factory patterns.
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);
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());
},
});
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 | 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 |
| 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 |
Two approaches to combine forms with server state:
Copy to Form State (Simple):
const { data } = useQuery(userOptions(id));
const [name, setName] = useState(data?.name ?? '');
// Form now independent of server state - loses background updates
Derived State (Advanced):
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 for full form patterns.
| 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 |
Explore agentTask agentcode-reviewer agent