tanstack-query-alternative
Skill10 starsChanged 6 months agoMITView on GitHub
Use when adding or refactoring data fetching with TanStack Query (React Query) in Lovable projects: useQuery, useMutation, query keys, cache invalidation, Supabase fetchers. This is the default data layer for Lovable. Not for forms validation alone or one-off fetches outside a hook.
In this repository56 other files it gives its agents.
- accessibility-passSkill
- analytics-eventsSkill
- api-error-handlingSkill
- audit-logging-backendSkill
- background-jobs-and-cronSkill
- dark-mode-and-themingSkill
- dashboard-charts-rechartsSkill
- edge-functions-and-webhooksSkill
- email-and-notificationsSkill
- error-states-and-empty-uiSkill
- feature-flags-and-togglesSkill
- file-upload-storageSkill
- i18n-multi-languageSkill
- lovable-prompting-playbookSkill
- lovable-ship-checklistSkill
- migration-playbookSkill
- multi-tenant-backendSkill
- onboarding-copySkill
- optimistic-updatesSkill
- payment-failed-and-recoverySkill
- payment-webhook-idempotencySkill
- payment-webhook-testingSkill
- performance-budgetSkill
- pg-cron-recipesSkill
- pg-cron-scheduled-jobsSkill
- pg-cron-with-pg-net-edgeSkill
- postgres-full-text-searchSkill
- postgres-triggers-and-functionsSkill
- pricing-and-billingSkill
- programmatic-seo-content-qualitySkill
- programmatic-seo-pagesSkill
- rate-limiting-edgeSkill
- realtime-and-subscriptionsSkill
- refactor-safe-diffSkill
- release-notesSkill
- seo-core-web-vitalsSkill
- seo-landing-pageSkill
- seo-meta-tags-spaSkill
- seo-sitemap-generationSkill
- seo-ssr-and-prerenderingSkill
- seo-structured-dataSkill
- server-input-validationSkill
- shadcn-patternsSkill
- soft-delete-and-archivingSkill
- stripe-one-time-payment-webhooksSkill
- stripe-payment-webhooksSkill
- supabase-auth-flowsSkill
- supabase-database-designSkill
- supabase-rls-and-authSkill
- support-replySkill
- tanstack-infinite-queriesSkill
- tanstack-mutations-and-invalidationSkill
- tanstack-prefetch-and-hydrationSkill
- tanstack-router-data-loadersSkill
- tanstack-start-server-routesSkill
- typed-api-hooks-formsSkill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 1 is distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 50
Never call supabase.from(...) inside a component.
- Very specific: line 86
Add ReactQueryDevtools in dev for cache inspection.
- Very specific: line 113
Don't sprinkle ad-hoc strings ("user-" + id).
- Distinctive: line 158
When several UI surfaces need the same Supabase graph on a screen, write one query with a joined select, not three:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
TanStack Query patterns (default for Lovable)
TanStack Query is Lovable's default data-fetching library. Use it for every server-state read and write. Reach for useEffect + fetch only when no cache is desired (e.g. one-shot edge POSTs without revalidation).
Pair with:
tanstack-mutations-and-invalidation— write pathstanstack-infinite-queries— pagination and infinite scrolltanstack-prefetch-and-hydration— route prefetching, SSR hydration
For SWR-first projects, use typed-api-hooks-forms instead. Pick one library per app — never mix.
File layout
src/
lib/
supabase.ts
query-client.ts # QueryClient + defaults
types/
database.ts # generated Supabase types
api/
users.ts # pure fetchers (no React)
projects.ts
hooks/
queries/
useUser.ts
useProjects.ts
mutations/
useCreateProject.ts
useDeleteProject.tsRules:
- Fetchers in
api/— async, typed, throw on error. - Hooks in
hooks/queries/— onlyuseQuerywiring; no JSX, no business logic. - Mutations in
hooks/mutations/—useMutation+ invalidation policy. - Never call
supabase.from(...)inside a component.
QueryClient defaults
// src/lib/query-client.ts
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000, // 30s — tune per resource
gcTime: 5 * 60 * 1000, // 5m
refetchOnWindowFocus: false, // Lovable apps usually don't need it
retry: (failureCount, err) => {
if (isHttpStatus(err, [401, 403, 404])) return false;
return failureCount < 2;
},
},
mutations: {
retry: false,
},
},
});Wrap the app once:
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>Add ReactQueryDevtools in dev for cache inspection.
Query keys — factory pattern
Centralize keys so invalidation can't drift:
// src/hooks/queries/keys.ts
export const userKeys = {
all: ["users"] as const,
detail: (id: string) => [...userKeys.all, "detail", id] as const,
};
export const projectKeys = {
all: ["projects"] as const,
list: (filters: { status?: string; orgId?: string }) =>
[...projectKeys.all, "list", filters] as const,
detail: (id: string) => [...projectKeys.all, "detail", id] as const,
};Rules:
- Tuple keys, broadest segment first (
["projects", "list", ...]). - Filter objects are fine — TanStack hashes them deterministically. But keep filter shapes stable.
- Don't sprinkle ad-hoc strings (
"user-" + id); always go through the factory.
Typed query hook
// src/api/users.ts
import type { Database } from "@/types/database";
type User = Database["public"]["Tables"]["users"]["Row"];
export async function fetchUser(id: string): Promise<User | null> {
const { data, error } = await supabase
.from("users")
.select("*")
.eq("id", id)
.maybeSingle();
if (error) throw error;
return data;
}// src/hooks/queries/useUser.ts
import { useQuery } from "@tanstack/react-query";
import { fetchUser } from "@/api/users";
import { userKeys } from "./keys";
export function useUser(id: string | null | undefined) {
return useQuery({
queryKey: id ? userKeys.detail(id) : userKeys.detail("none"),
queryFn: () => fetchUser(id!),
enabled: !!id,
});
}The hook returns the full UseQueryResult — components destructure { data, isLoading, error, refetch } directly. Do not invent custom return shapes that lose generics.
Deduping — same key = one request
Three components calling useUser(id) with the same id share one network request and one cache entry. Get this for free by using the key factory everywhere.
When several UI surfaces need the same Supabase graph on a screen, write one query with a joined select, not three:
// api/posts.ts
export async function fetchPostDetail(postId: string) {
const { data, error } = await supabase
.from("posts")
.select(`
*,
author:users!posts_author_id_fkey ( id, display_name, avatar_url ),
comments ( id, body, created_at, author:users ( id, display_name ) )
`)
.eq("id", postId)
.single();
if (error) throw error;
return data;
}
// hooks/queries/usePostDetail.ts
export function usePostDetail(postId: string | undefined) {
return useQuery({
queryKey: postKeys.detail(postId ?? "none"),
queryFn: () => fetchPostDetail(postId!),
enabled: !!postId,
});
}Strategy matrix:
| Situation | Approach |
| Same entity, many components | One hook + shared key |
| Same table, different filters | One hook, distinct keys per filter |
| List + detail with overlap | List hook for list; detail hook with richer select |
| Many components need full graph | Single joined fetcher + one hook |
Loading and error UX
isLoading(first load) vsisFetching(background refetch) — only show skeletons onisLoading.isError: show retry callingrefetch().- Pair with
error-states-and-empty-ui.
Mutations (overview — see deep dive)
const m = useMutation({
mutationFn: api.createProject,
onSuccess: () => qc.invalidateQueries({ queryKey: projectKeys.all }),
});Optimistic UI, complex invalidation, and rollback: see tanstack-mutations-and-invalidation and optimistic-updates.
Avoid
useEffect+fetchfor server state.- Inline Supabase calls in components.
- Mixing SWR and TanStack Query in one project.
- Ad-hoc string keys outside the factory.
- Wrapping
useQueryin a custom shape that hidesisFetching,dataUpdatedAt, orerror. refetchOnWindowFocus: trueon heavy queries (kills cost when users tab back).
Checklist
- Single
QueryClientat the app root with sane defaults. - Key factories per resource, used everywhere.
- Typed fetchers in
api/; hooks inhooks/queries. - One joined query per screen instead of N parallel ones.
- DevTools mounted in dev for cache inspection.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

