È uscito il Corso SQL Completo

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,
}