Query Keys
Struttura delle Query Keys
Le query keys sono array che identificano univocamente una query nella cache. TanStack Query usa queste chiavi per il caching, la deduplicazione e l’invalidazione.
// Key semplice - lista di tutti i todos
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
// Key con parametro - singolo todo
useQuery({ queryKey: ['todos', todoId], queryFn: () => fetchTodo(todoId) })
// Key con filtri
useQuery({
queryKey: ['todos', { status: 'active', page: 1 }],
queryFn: () => fetchTodos({ status: 'active', page: 1 }),
})
Array Keys
Le chiavi sono sempre array, anche con un solo elemento. TanStack Query confronta le chiavi tramite deep equality:
// Queste sono la STESSA query (ordine dell'oggetto non conta)
useQuery({ queryKey: ['todos', { page: 1, status: 'active' }], ... })
useQuery({ queryKey: ['todos', { status: 'active', page: 1 }], ... })
// Queste sono query DIVERSE (ordine degli elementi dell'array conta)
useQuery({ queryKey: ['todos', 1], ... })
useQuery({ queryKey: [1, 'todos'], ... })
Oggetti nelle Keys
Gli oggetti all’interno delle chiavi sono confrontati per valore, non per riferimento:
interface TodoFilters {
status: 'all' | 'active' | 'completed'
sortBy: 'date' | 'priority'
}
function useTodos(filters: TodoFilters) {
return useQuery({
queryKey: ['todos', filters],
queryFn: () => fetchTodos(filters),
})
}
// Uso nel componente
function TodoList() {
const [filters, setFilters] = useState<TodoFilters>({
status: 'active',
sortBy: 'date',
})
// La query cambia automaticamente quando cambiano i filtri
const { data } = useTodos(filters)
return (
<div>
<select onChange={(e) => setFilters(f => ({ ...f, status: e.target.value as any }))}>
<option value="all">Tutti</option>
<option value="active">Attivi</option>
<option value="completed">Completati</option>
</select>
{data?.map(todo => <TodoItem key={todo.id} todo={todo} />)}
</div>
)
}
Query Key Factory
Per progetti di medie/grandi dimensioni, e’ consigliato creare una factory che centralizza tutte le chiavi:
// keys/todo.keys.ts
export const todoKeys = {
all: ['todos'] as const,
lists: () => [...todoKeys.all, 'list'] as const,
list: (filters: TodoFilters) => [...todoKeys.lists(), filters] as const,
details: () => [...todoKeys.all, 'detail'] as const,
detail: (id: number) => [...todoKeys.details(), id] as const,
}
Uso della factory:
// Fetch lista con filtri
useQuery({
queryKey: todoKeys.list({ status: 'active', sortBy: 'date' }),
queryFn: () => fetchTodos({ status: 'active', sortBy: 'date' }),
})
// Fetch singolo todo
useQuery({
queryKey: todoKeys.detail(5),
queryFn: () => fetchTodo(5),
})
Factory per Entita’ Multiple
// keys/index.ts
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (params: UserParams) => [...userKeys.lists(), params] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
profile: (id: string) => [...userKeys.detail(id), 'profile'] as const,
}
export const postKeys = {
all: ['posts'] as const,
byUser: (userId: string) => [...postKeys.all, 'user', userId] as const,
detail: (id: string) => [...postKeys.all, 'detail', id] as const,
comments: (postId: string) => [...postKeys.detail(postId), 'comments'] as const,
}
Invalidazione Basata su Prefix
L’invalidazione funziona con matching parziale: se invalidi una chiave, tutte le query che iniziano con quella chiave vengono invalidate.
const queryClient = useQueryClient()
// Invalida TUTTE le query dei todos (lista, dettagli, filtri)
queryClient.invalidateQueries({ queryKey: todoKeys.all })
// Invalida: ['todos'], ['todos', 'list', ...], ['todos', 'detail', ...]
// Invalida solo le liste (non i dettagli)
queryClient.invalidateQueries({ queryKey: todoKeys.lists() })
// Invalida: ['todos', 'list'], ['todos', 'list', { status: 'active' }]
// Invalida un singolo dettaglio
queryClient.invalidateQueries({ queryKey: todoKeys.detail(5) })
// Invalida: ['todos', 'detail', 5]
Invalidazione con Filtri Avanzati
// Invalida solo le query attive (montate in un componente)
queryClient.invalidateQueries({
queryKey: ['todos'],
type: 'active',
})
// Invalida solo le query inattive
queryClient.invalidateQueries({
queryKey: ['todos'],
type: 'inactive',
})
// Invalida con un predicato custom
queryClient.invalidateQueries({
predicate: (query) => {
return query.queryKey[0] === 'todos' && query.state.dataUpdatedAt < Date.now() - 60000
},
})
Convenzioni di Naming
Ecco le convenzioni consigliate per mantenere le chiavi organizzate:
// 1. Usa nomi di entita' al plurale come primo elemento
['users'], ['posts'], ['comments']
// 2. Aggiungi 'list' o 'detail' come secondo livello
['users', 'list'], ['users', 'detail', userId]
// 3. Aggiungi filtri come oggetto nell'ultimo elemento
['users', 'list', { role: 'admin', page: 2 }]
// 4. Per relazioni, usa chiavi annidate
['users', userId, 'posts'] // posts di un utente specifico
// 5. Centralizza tutto in una factory
export const keys = {
users: userKeys,
posts: postKeys,
comments: commentKeys,
}