Skip to main content
Query keys are the foundation of TanStack Query’s caching system. They uniquely identify queries and are used to manage cache data, invalidation, and refetching.

Query Key Structure

Query keys must be arrays. The simplest form is an array with a single string:
// Simple key
useQuery({ queryKey: ['posts'], queryFn: fetchPosts })
Query keys can include multiple elements for hierarchical organization:
// Hierarchical keys
useQuery({ queryKey: ['posts', 'list'], queryFn: fetchPosts })
useQuery({ queryKey: ['posts', 'detail', postId], queryFn: () => fetchPost(postId) })

Type Definition

From types.ts:53-61, query keys are defined as:
export type QueryKey = Register extends {
  queryKey: infer TQueryKey
}
  ? TQueryKey extends ReadonlyArray<unknown>
    ? TQueryKey
    : TQueryKey extends Array<unknown>
      ? TQueryKey
      : ReadonlyArray<unknown>
  : ReadonlyArray<unknown>
This means query keys are always arrays and can contain any serializable values.
TanStack Query v4+ requires query keys to be arrays. If you’re migrating from v3, change string keys like 'posts' to ['posts'].

Query Key Best Practices

1. Hierarchical Structure

Organize keys from generic to specific:
// Good: hierarchical structure
useQuery({ queryKey: ['users'], queryFn: fetchUsers })
useQuery({ queryKey: ['users', userId], queryFn: () => fetchUser(userId) })
useQuery({ queryKey: ['users', userId, 'posts'], queryFn: () => fetchUserPosts(userId) })

// This allows partial invalidation:
queryClient.invalidateQueries({ queryKey: ['users'] }) // Invalidates all user-related queries
queryClient.invalidateQueries({ queryKey: ['users', userId] }) // Invalidates specific user

2. Include All Dependencies

Include all variables that the query function depends on:
function usePosts(filters: { status: string; page: number }) {
  return useQuery({
    queryKey: ['posts', filters], // Include filters in the key
    queryFn: () => fetchPosts(filters),
  })
}

// Different filters = different queries
usePosts({ status: 'published', page: 1 })
usePosts({ status: 'draft', page: 1 })

3. Consistent Ordering

Maintain consistent ordering of key elements:
// Good: consistent structure
useQuery({ queryKey: ['posts', { status, sort }], ... })
useQuery({ queryKey: ['posts', { status, sort }], ... })

// Bad: inconsistent structure
useQuery({ queryKey: ['posts', { status, sort }], ... })
useQuery({ queryKey: ['posts', { sort, status }], ... }) // Different order
Object keys in query keys are not automatically sorted. Use a consistent order or the queries will be treated as different.

Query Key Serialization

Query keys are serialized to a query hash using the hashQueryKeyByOptions function. This hash is used internally for cache lookups. From queryCache.ts:113-116:
const queryKey = options.queryKey
const queryHash =
  options.queryHash ?? hashQueryKeyByOptions(queryKey, options)
let query = this.get<TQueryFnData, TError, TData, TQueryKey>(queryHash)
The hash function serializes the key deterministically:
// These create the same hash
queryKey: ['posts', { id: 1, status: 'active' }]
queryKey: ['posts', { id: 1, status: 'active' }]

// These create different hashes
queryKey: ['posts', { id: 1, status: 'active' }]
queryKey: ['posts', { status: 'active', id: 1 }] // Different object key order

Array-Based Keys

Simple Values

Arrays can contain strings, numbers, booleans, etc.:
useQuery({ queryKey: ['posts'], ... })
useQuery({ queryKey: ['post', 123], ... })
useQuery({ queryKey: ['user', userId, 'active'], ... })

Objects in Keys

Objects are useful for complex filters:
const filters = {
  status: 'published',
  author: 'john',
  tags: ['react', 'typescript'],
}

useQuery({
  queryKey: ['posts', filters],
  queryFn: () => fetchPosts(filters),
})

Nested Objects

Nested structures are supported:
useQuery({
  queryKey: ['posts', {
    filters: { status: 'published' },
    pagination: { page: 1, pageSize: 10 },
    sort: { field: 'createdAt', order: 'desc' },
  }],
  queryFn: () => fetchPosts(...),
})

Query Key Factories

Create factory functions for consistent key generation:
const postKeys = {
  all: ['posts'] as const,
  lists: () => [...postKeys.all, 'list'] as const,
  list: (filters: PostFilters) => [...postKeys.lists(), filters] as const,
  details: () => [...postKeys.all, 'detail'] as const,
  detail: (id: number) => [...postKeys.details(), id] as const,
}

// Usage
useQuery({ queryKey: postKeys.all, ... })
useQuery({ queryKey: postKeys.list({ status: 'published' }), ... })
useQuery({ queryKey: postKeys.detail(123), ... })

// Invalidation
queryClient.invalidateQueries({ queryKey: postKeys.all }) // All posts
queryClient.invalidateQueries({ queryKey: postKeys.lists() }) // All lists
queryClient.invalidateQueries({ queryKey: postKeys.detail(123) }) // Specific post
Use as const assertions to ensure type safety and prevent accidental mutations of query keys.

Query Key Matching

Exact Matching

By default, queries match exactly:
// Only invalidates queries with exactly ['posts']
queryClient.invalidateQueries({ queryKey: ['posts'], exact: true })

Prefix Matching

Without exact: true, keys match by prefix:
// Invalidates:
// ['posts']
// ['posts', 'list']
// ['posts', 'detail', 123]
queryClient.invalidateQueries({ queryKey: ['posts'] })

Partial Matching

The matchQuery function (used internally) supports partial matching:
// From queryCache.ts:183-191
find<TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData>(
  filters: WithRequired<QueryFilters, 'queryKey'>,
): Query<TQueryFnData, TError, TData> | undefined {
  const defaultedFilters = { exact: true, ...filters }

  return this.getAll().find((query) =>
    matchQuery(defaultedFilters, query),
  ) as Query<TQueryFnData, TError, TData> | undefined
}

Query Key in Query Function

The query key is available in the query function context:
useQuery({
  queryKey: ['posts', { status: 'published', page: 1 }],
  queryFn: ({ queryKey }) => {
    const [_key, filters] = queryKey
    return fetchPosts(filters)
  },
})
From the source, the context includes the queryKey at query.ts:450-461:
const createQueryFnContext = (): QueryFunctionContext<TQueryKey> => {
  const queryFnContext: OmitKeyof<
    QueryFunctionContext<TQueryKey>,
    'signal'
  > = {
    client: this.#client,
    queryKey: this.queryKey,
    meta: this.meta,
  }
  addSignalProperty(queryFnContext)
  return queryFnContext as QueryFunctionContext<TQueryKey>
}

Custom Query Hash Function

You can provide a custom hash function:
import { QueryClient } from '@tanstack/react-query'
import { hashKey } from '@tanstack/react-query'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      queryKeyHashFn: (queryKey) => {
        // Custom hashing logic
        return hashKey(queryKey)
      },
    },
  },
})

Effective Key Organization

By Resource

const keys = {
  users: {
    all: ['users'] as const,
    detail: (id: number) => ['users', id] as const,
  },
  posts: {
    all: ['posts'] as const,
    list: (filters: Filters) => ['posts', 'list', filters] as const,
    detail: (id: number) => ['posts', id] as const,
  },
}

By Feature

const dashboardKeys = {
  all: ['dashboard'] as const,
  stats: (period: string) => ['dashboard', 'stats', period] as const,
  activity: (userId: number) => ['dashboard', 'activity', userId] as const,
}

Type Safety with Query Keys

Use TypeScript to ensure type safety:
type PostFilters = {
  status: 'published' | 'draft'
  author?: string
}

const postKeys = {
  all: ['posts'] as const,
  list: (filters: PostFilters) => ['posts', 'list', filters] as const,
  detail: (id: number) => ['posts', 'detail', id] as const,
}

// Type-safe usage
const filters: PostFilters = { status: 'published' }
useQuery({ queryKey: postKeys.list(filters), ... })

// TypeScript error: invalid status
// postKeys.list({ status: 'invalid' })

Query Key Examples from Source

From the test suite in useQuery.test.tsx:46-53:
function Page() {
  const { data = 'default' } = useQuery({
    queryKey: key,
    queryFn: () => sleep(10).then(() => 'test'),
  })

  return <div><h1>{data}</h1></div>
}
And from the basic example in examples/react/basic/src/index.tsx:26-34:
function usePosts() {
  return useQuery({
    queryKey: ['posts'],
    queryFn: async (): Promise<Array<Post>> => {
      const response = await fetch('https://jsonplaceholder.typicode.com/posts')
      return await response.json()
    },
  })
}

Build docs developers (and LLMs) love