Query Key Factories
Query Key Factories
TanStack Query identifies cached data by query key. Two calls with different keys are treated as different data , invalidateQueries with a key that doesn't match the original queryOptions key silently does nothing.
TypeScript won't catch the mismatch. invalidateQueries accepts unknown[], so ['myItems'] and ['my-items'] both type-check even though they refer to different cache entries.
The pattern
All query keys are factory functions in src/lib/query-keys.ts. Import the same factory in both queryOptions and invalidateQueries.
// src/lib/query-keys.ts
export const notesQueryKey = () => ['notes'] as const;
export const noteDetailQueryKey = (id: string) => ['notes', id] as const;
// In the feature barrel (index.ts):
import { queryOptions } from '@tanstack/react-query';
import { notesQueryKey } from '@/lib/query-keys';
import { getNotesFn } from './server/notes.queries';
export const notesQueryOptions = queryOptions({
queryKey: notesQueryKey(),
queryFn: async () => {
const result = await getNotesFn();
if (!result.ok) throw new Error(result.message);
return result.data;
},
staleTime: 30_000
});
// In a mutation (after write, invalidate via factory):
import { notesQueryKey } from '@/lib/query-keys';
await queryClient.invalidateQueries({ queryKey: notesQueryKey() });
A factory import error is a compile error. An inline string mismatch is invisible.
Where queryOptions live
queryOptions objects belong in the feature barrel (features/*/index.ts), not inline in route files. Route files import and use them:
// Route file:
import { notesQueryOptions } from '@/features/notes';
export const Route = createFileRoute('/_app/dashboard/notes/')({
loader: ({ context }) => context.queryClient.ensureQueryData(notesQueryOptions),
component: NotesPage
});
This makes queryOptions reusable across routes and keeps route files thin.
Testability
src/lib/query-keys.ts imports nothing from the server layer. It's safe to import in bun:test without triggering server function registration side effects:
import { notesQueryKey } from '@/lib/query-keys';
// No server fn imports → no side effects in test env
Adding a new query key
Add to src/lib/query-keys.ts:
export const myFeatureQueryKey = () => ['my-feature'] as const;
export const myFeatureDetailQueryKey = (id: string) => ['my-feature', id] as const;
Then use the factory everywhere , queryOptions, invalidateQueries, prefetchQuery. Never write the key as an inline array.