React Principles — React Query Hook Scaffold
You scaffold a TanStack Query (React Query) hook following the Server State with React Query recipe.
When to invoke
- User asks to "create a query hook" or "fetch data from an API"
- User asks for
useQuery/useMutationscaffolding - User mentions TanStack Query, React Query, or server state
Critical check first
Before generating, confirm with the user that this is server state, not client state.
- ✅ Use React Query for: API responses, paginated lists, user data fetched from server, search results
- ❌ Do NOT use React Query for: UI toggles, filter state, theme — use Zustand instead (
reactprinciples-storeskill)
Inputs needed
Ask the user for:
- Hook name — camelCase starting with
use(e.g.,useUsers,useUser,useSearchUsers) - Query type:
- List — paginated/filtered list (typically uses
staleTime+placeholderData) - Detail — single resource by id (typically uses
enabled: !!id) - Search — debounced search (typically uses
enabled: query.length > 0) - Mutation — POST/PUT/PATCH/DELETE (uses
useMutation+ cache invalidation)
- List — paginated/filtered list (typically uses
- Service method — which method on which service (e.g.,
usersService.getAll,usersService.getById) - Query key — which key from
queryKeysfactory (e.g.,queryKeys.users.list(params)) - Location —
src/features/<feature>/hooks/
What to read first
Read existing hooks for reference:
src/features/examples/hooks/useUsers.ts # list with staleTime + placeholderData
src/features/examples/hooks/useUser.ts # detail with enabled
src/features/examples/hooks/useSearchUsers.ts # debounced search
src/features/examples/hooks/useCreateUser.ts # mutation with invalidation
src/lib/query-keys.ts # query keys factory
src/lib/services/users.ts # service layer
Confirm queryKeys has the needed entry — if not, instruct the user to add it.
Templates
List query
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "@/lib/query-keys";
import { <service>, type Get<Resource>Params } from "@/lib/services/<service-file>";
export function use<Resources>(params: Get<Resource>Params = {}) {
return useQuery({
queryKey: queryKeys.<resource>.list(params),
queryFn: () => <service>.getAll(params),
staleTime: 1000 * 60 * 5, // 5 minutes
placeholderData: (prev) => prev, // smooth pagination
});
}
Detail query
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "@/lib/query-keys";
import { <service> } from "@/lib/services/<service-file>";
export function use<Resource>(id: string) {
return useQuery({
queryKey: queryKeys.<resource>.detail(id),
queryFn: () => <service>.getById(id),
enabled: !!id,
staleTime: 1000 * 60 * 10, // 10 minutes for stable details
});
}
Debounced search
import { useQuery } from "@tanstack/react-query";
import { useDebounce } from "@/shared/hooks";
import { queryKeys } from "@/lib/query-keys";
import { <service> } from "@/lib/services/<service-file>";
export function useSearch<Resources>(query: string) {
const debouncedQuery = useDebounce(query, 300);
return useQuery({
queryKey: queryKeys.<resource>.search(debouncedQuery),
queryFn: () => <service>.search({ q: debouncedQuery }),
enabled: debouncedQuery.length > 0,
staleTime: 1000 * 60 * 5,
});
}
Mutation
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/query-keys";
import { <service> } from "@/lib/services/<service-file>";
import type { Create<Resource>Input } from "@/shared/types/<resource>";
export function useCreate<Resource>() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Create<Resource>Input) => <service>.create(data),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: queryKeys.<resource>.all });
},
});
}
Rules embedded in the templates
- Always set
staleTimeexplicitly — defaults are too aggressive for most apps. 5 min for lists, 10 min for details is a sensible baseline. placeholderData: (prev) => prevfor paginated lists — prevents layout shift.enabledflag for dependent queries — never run a query before its input exists.- Mutations invalidate the relevant list cache via
queryClient.invalidateQueries. - Void the invalidate —
void queryClient.invalidateQueries(...)because we don't await it.
After generating
Tell the user:
- The file path created
- Import path:
import { use<Resources> } from "@/features/<feature>/hooks/use<Resources>" - If
queryKeys.<resource>doesn't exist yet, instruct them to add it tosrc/lib/query-keys.ts - If the service method doesn't exist yet, instruct them to add it to the appropriate service file
- Suggest pairing with
HydrationBoundary+dehydratefor SSR prefetch in Next.js page components
What you should NOT do
- Don't use
fetch()oraxiosdirectly in the hook — call a service method that usescreateApiClient - Don't omit
staleTime— explicit is better than relying on defaults - Don't put the query hook in
src/components/— hooks go insrc/features/<x>/hooks/ - Don't mix server state (React Query) and client state (Zustand) in the same hook
Reference
See Server State with React Query recipe and existing hooks in src/features/examples/hooks/.