Define hierarchical query keys and options in factories
Build each entity's query keys from general to specific, such as entity, then kind, then ID or filters, and define them, together with their query functions, in one factory per entity. Use the factory everywhere the query is read, prefetched, updated, or invalidated.
Implementation
- Start every key for an entity with the same prefix, such as
['todos'], then add a kind such as'list'or'detail', then the ID or filters. - Define factory functions that return
queryOptions({ queryKey, queryFn, ... }), so the key, query function, and per-query options stay together and types flow touseQuery,prefetchQuery, andgetQueryData. - Expose prefix helpers, such as
todoQueries.all()ortodoKeys.lists(), for invalidation at each level. - A small app with a handful of queries can inline keys; add a factory when the same key is written in more than one place.
Rationale
TanStack Query matches keys by prefix for invalidation and cache filters.
A consistent hierarchy lets one call target exactly the right level, such as every list of todos or one todo and its sub-resources.
Keys typed by hand in many files drift, such as 'todo' in one place and 'todos' in another, and the mismatch silently skips cache entries.
Examples
Incorrect (counterexample):
useQuery({ queryKey: ['todos', 'list', filters], queryFn: () => fetchTodos(filters) });
useQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) });
queryClient.invalidateQueries({ queryKey: ['todos'] });
The detail query uses 'todo', so invalidating ['todos'] misses it.
Correct:
export const todoQueries = {
all: () => ['todos'] as const,
lists: () => [...todoQueries.all(), 'list'] as const,
list: (filters: TodoFilters) =>
queryOptions({
queryKey: [...todoQueries.lists(), filters] as const,
queryFn: () => fetchTodos(filters),
}),
detail: (id: number) =>
queryOptions({
queryKey: [...todoQueries.all(), 'detail', id] as const,
queryFn: () => fetchTodo(id),
staleTime: 5 * 60 * 1000,
}),
};
useQuery(todoQueries.detail(id));
await queryClient.prefetchQuery(todoQueries.list({ status: 'active' }));
queryClient.invalidateQueries({ queryKey: todoQueries.lists() });
queryClient.invalidateQueries({ queryKey: todoQueries.all() });
Validation
Search for query keys written as literals outside the factory, and check that each entity's keys share one prefix.
Inline keys in a small app where each key appears once are not a violation.