Skip to main content
Attributes: ✅ Recommended • 🔧 Fixable

Overview

Query keys should be seen like a dependency array to your query function: Every variable that is used inside the queryFn should be added to the query key. This makes sure that queries are cached independently and that queries are refetched automatically when the variables change.

Rule Details

This rule enforces that all variables referenced in your query function are included in the query key. This is crucial for proper caching and automatic refetching behavior.

Why This Matters

  1. Independent Caching: Different values should create different cache entries
  2. Automatic Refetching: Queries refetch when dependencies change
  3. Stale Data Prevention: Ensures you’re always working with the correct data

Examples

Incorrect Code

/* eslint "@tanstack/query/exhaustive-deps": "error" */

useQuery({
  queryKey: ['todo'],
  queryFn: () => api.getTodo(todoId), // ❌ todoId not in key
})

Correct Code

useQuery({
  queryKey: ['todo', todoId], // ✅ todoId included
  queryFn: () => api.getTodo(todoId),
})

Common Patterns

User-Specific Queries

function useUserTodos(userId: string) {
  return useQuery({
    queryKey: ['todos', userId],
    queryFn: () => fetchUserTodos(userId),
  })
}

Filtered Lists

function useTodos(filters: TodoFilters) {
  return useQuery({
    queryKey: ['todos', filters],
    queryFn: () => fetchTodos(filters),
  })
}

Paginated Data

function useTodos(page: number, pageSize: number) {
  return useQuery({
    queryKey: ['todos', page, pageSize],
    queryFn: () => fetchTodos(page, pageSize),
  })
}

Detail Views

function useTodo(id: string, includeComments: boolean) {
  return useQuery({
    queryKey: ['todo', id, { includeComments }],
    queryFn: () => fetchTodo(id, includeComments),
  })
}

What About Constants?

Constants that never change don’t need to be in the query key:
const API_URL = 'https://api.example.com' // constant

useQuery({
  queryKey: ['todos'], // ✅ OK: API_URL doesn't need to be in key
  queryFn: () => fetch(`${API_URL}/todos`),
})
However, configuration that might change should be included:
const { apiUrl } = useConfig() // can change

useQuery({
  queryKey: ['todos', apiUrl], // ✅ Include changing config
  queryFn: () => fetch(`${apiUrl}/todos`),
})

Auto-Fix

This rule is auto-fixable. ESLint can automatically add missing dependencies to your query key:
eslint --fix src/
Review auto-fixes carefully. The rule may not always detect the optimal query key structure.

When to Disable

You might want to disable this rule if:
  1. You’re using a custom query key factory that handles dependencies differently
  2. You’re intentionally sharing cache across different parameters (advanced use case)
  3. You’re using a global query function that doesn’t depend on variables
// eslint-disable-next-line @tanstack/query/exhaustive-deps
useQuery({
  queryKey: ['constant-data'],
  queryFn: () => fetchConstantData(dynamicParam),
})
Disabling this rule can lead to stale data and caching bugs. Only do so if you fully understand the implications.

Best Practices

Create reusable query configurations with proper key structures:
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: string) => [...todoKeys.details(), id] as const,
}

// Usage
useQuery({
  queryKey: todoKeys.detail(id),
  queryFn: () => fetchTodo(id),
})
Ensure all query key values are serializable:
// ✅ Good: primitive values and plain objects
queryKey: ['todos', { status: 'active', page: 1 }]

// ❌ Bad: functions, class instances
queryKey: ['todos', () => {}, new Date()]
Keep related queries together by using consistent key ordering:
// ✅ Good: consistent hierarchy
['todos'] // all todos
['todos', { status: 'active' }] // filtered todos
['todos', 'detail', id] // specific todo

// ❌ Bad: inconsistent structure
['todos']
[{ status: 'active' }, 'todos']
[id, 'todos', 'detail']

TypeScript Support

The rule works seamlessly with TypeScript and understands type information:
interface TodoFilters {
  status: 'active' | 'completed'
  priority?: 'high' | 'low'
}

function useTodos(filters: TodoFilters) {
  return useQuery({
    queryKey: ['todos', filters], // ✅ Properly typed
    queryFn: () => fetchTodos(filters),
  })
}

Further Reading

Query Keys Guide

Learn best practices for structuring query keys

Effective Query Keys

TkDodo’s guide to query key patterns

Build docs developers (and LLMs) love