tanstack-query
GitHub提供 TanStack Query v5 的服务端状态管理最佳实践,涵盖查询定义、缓存键规范、Suspense 及突变模式,指导开发者高效处理数据获取与缓存。
Trigger Scenarios
Install
npx skills add TangleML/tangle-ui --skill tanstack-query -g -y
SKILL.md
Frontmatter
{
"name": "tanstack-query",
"description": "TanStack Query v5 patterns for data fetching, mutations, and cache management. Use when writing queries, mutations, or working with server state."
}
TanStack Query Patterns
This project uses TanStack Query v5 for all server state management.
Prefer useQuery hooks over Context providers for server state. When you need data from the server, first consider whether a custom useQuery hook solves the problem. Only reach for a Context provider when you need to share non-query app-wide state (theme, feature flags, backend config). Wrapping query results in Context bypasses TanStack Query's built-in caching and causes unnecessary re-renders.
Query Key Conventions
Use hierarchical array-based keys. For domains with multiple related queries, use a query key factory:
export const SecretsQueryKeys = {
All: () => ["secrets"] as const,
Id: (id: string) => ["secrets", id] as const,
} as const;
Standalone queries just use a simple key:
queryKey: ["pipeline-run", rootExecutionId];
queryKey: ["execution-details", rootExecutionId];
queryKey: ["component", "hydrate", componentQueryKey];
Query Definition
Define queries inline in custom hooks — this project does not use the queryOptions helper:
export function usePipelineRuns(pipelineName?: string) {
return useSuspenseQuery({
queryKey: ["pipelineRuns", pipelineName],
queryFn: async () => {
if (!pipelineName) return [];
const res = await fetchPipelineRuns(pipelineName);
if (!res) return [];
return res.runs;
},
staleTime: 5 * MINUTES,
refetchOnWindowFocus: false,
refetchOnMount: false,
});
}
Suspense Queries
Use useSuspenseQuery for components wrapped in <SuspenseWrapper> or error boundaries:
export function useHydrateComponentReference(component: ComponentReference) {
const { data } = useSuspenseQuery({
queryKey: ["component", "hydrate", getComponentQueryKey(component)],
staleTime: 1000 * 60 * 60,
queryFn: () => hydrateComponentReference(component),
});
return data;
}
Dependent Queries
Chain queries using the enabled option:
const { data: rootExecutionId } = useQuery({
queryKey: ["pipeline-run-execution-id", id],
queryFn: () =>
fetchPipelineRun(id, backendUrl).then((res) => res.root_execution_id),
enabled: !!id && id.length > 0,
});
const { data: executionData } = useQuery({
enabled: !!rootExecutionId && !!executionDetails,
queryKey: ["pipeline-run", rootExecutionId],
queryFn: () => fetchData(rootExecutionId),
});
Mutation Pattern
All mutations follow this structure — invalidate cache on success, toast on error:
const { mutate, isPending } = useMutation({
mutationFn: () => addSecret(secret),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: SecretsQueryKeys.All() });
onSuccess();
},
onError: () => {
notify("Failed to add secret", "error");
},
});
Multiple invalidations in a single mutation are fine:
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["has-component", digest] });
queryClient.invalidateQueries({ queryKey: ["componentLibrary", "publishedComponents"] });
},
Cache Invalidation Strategy
This project uses post-mutation invalidation, not optimistic updates. Do not use setQueryData in mutations — invalidate and let the query refetch.
QueryClient methods used:
queryClient.invalidateQueries()— primary invalidationqueryClient.fetchQuery()— direct fetch in non-hook contexts (e.g., class-based libraries)queryClient.getQueryData()— cache reading without triggering refetchqueryClient.ensureQueryData()— fetch-if-not-cached for recursive/dependent data
Stale Time Guidelines
Match stale time to data volatility:
| Data Type | Stale Time | Example |
|---|---|---|
| Immutable/rare changes | 24 hours | Pipeline run metadata, component digests |
| User profile data | 30 minutes | User details |
| Semi-stable data | 1 hour | Execution details, component hydration |
| Active lists | 5 minutes | Pipeline runs, published components, outdated checks |
| Live/polling data | 5 seconds | Logs |
| Always fresh | 0 | User components |
Use time constants from src/utils/constants.ts: ONE_MINUTE_IN_MS, MINUTES, HOURS, TWENTY_FOUR_HOURS_IN_MS.
Polling with Dynamic Intervals
Use refetchInterval with a function for conditional polling:
refetchInterval: (data) => {
if (data instanceof Query) {
const { state } = data.state.data || {};
if (!state) return false;
return isExecutionComplete(stats) ? false : 5000;
}
return false;
},
Error Handling
- Use
.catch(() => undefined)for controlled fallbacks in queryFn - Use
onErrorwith toast notifications in mutations - Create custom error classes for domain-specific errors (e.g.,
ComponentHydrationError)
File Organization
- Service functions (API calls):
src/services/ - Query hooks:
src/hooks/or co-located in component directories - Query key factories: co-located with the feature (e.g.,
types.tsin the feature folder) - Providers using queries:
src/providers/
Version History
- d7768e8 Current 2026-09-02 20:59


