Back to Ragflow

qk-factory-pattern: Use Query Key Factories for Complex Applications

web/.agents/skills/tanstack-query-best-practices/rules/qk-factory-pattern.md

0.25.13.0 KB
Original Source

qk-factory-pattern: Use Query Key Factories for Complex Applications

Priority: CRITICAL

Explanation

For applications with many queries, centralize query key definitions in factory functions. This ensures consistency, enables autocomplete, prevents typos, and makes refactoring safer. Query key factories are the recommended pattern for production applications.

Bad Example

tsx
// Scattered, inconsistent key definitions across files
// file: components/TodoList.tsx
const { data } = useQuery({
  queryKey: ['todos', 'list'],
  queryFn: fetchTodos,
})

// file: components/TodoDetail.tsx
const { data } = useQuery({
  queryKey: ['todo', id],  // Inconsistent: 'todo' vs 'todos'
  queryFn: () => fetchTodo(id),
})

// file: components/TodoComments.tsx
const { data } = useQuery({
  queryKey: ['todoComments', todoId],  // Different naming convention
  queryFn: () => fetchComments(todoId),
})

// Invalidation is error-prone
queryClient.invalidateQueries({ queryKey: ['todos'] })  // Misses 'todo' and 'todoComments'

Good Example

tsx
// file: lib/query-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,
  comments: (id: number) => [...todoKeys.detail(id), 'comments'] as const,
}

export const userKeys = {
  all: ['users'] as const,
  detail: (id: string) => [...userKeys.all, id] as const,
  posts: (id: string) => [...userKeys.detail(id), 'posts'] as const,
}

// file: components/TodoList.tsx
import { todoKeys } from '@/lib/query-keys'

const { data } = useQuery({
  queryKey: todoKeys.list({ status: 'active' }),
  queryFn: () => fetchTodos({ status: 'active' }),
})

// file: components/TodoDetail.tsx
const { data } = useQuery({
  queryKey: todoKeys.detail(id),
  queryFn: () => fetchTodo(id),
})

// Invalidation is type-safe and predictable
queryClient.invalidateQueries({ queryKey: todoKeys.all })  // Invalidates everything
queryClient.invalidateQueries({ queryKey: todoKeys.detail(5) })  // Specific todo + comments

Query Options Factory Pattern

tsx
// Even better: combine with queryOptions for full type safety
import { queryOptions } from '@tanstack/react-query'

export const todoQueries = {
  all: () => queryOptions({
    queryKey: todoKeys.all,
    queryFn: fetchAllTodos,
  }),
  detail: (id: number) => queryOptions({
    queryKey: todoKeys.detail(id),
    queryFn: () => fetchTodo(id),
    staleTime: 5 * 60 * 1000,
  }),
}

// Usage
const { data } = useQuery(todoQueries.detail(5))
await queryClient.prefetchQuery(todoQueries.detail(5))

Context

  • Essential for applications with 10+ different query types
  • Enables IDE autocomplete and typo prevention
  • Makes invalidation patterns discoverable
  • Pairs well with queryOptions for full type inference
  • Consider the @lukemorales/query-key-factory package for standardized implementation