Query Keys
Understanding query key structure and cache operations
Query Keys
Eden TanStack Query generates deterministic query keys from your route paths and input. These keys are used by TanStack Query for caching, deduplication, and invalidation.
Key Structure
Query keys follow this structure:
// queryKey() returns:
[['path', 'segments', 'method'], { input: { ... }, type: 'query' }]
// Examples:
eden.users.get.queryKey()
// => [['users', 'get'], { type: 'query' }]
eden.users.get.queryKey({ role: 'admin' })
// => [['users', 'get'], { input: { role: 'admin' }, type: 'query' }]
eden.users({ id: '1' }).get.queryKey()
// => [['users', 'get'], {
// pathParams: [{ pathIndex: 0, entries: [['id', '1']] }],
// type: 'query'
// }]The type field differentiates regular queries from infinite queries:
eden.posts.get.infiniteQueryKey({ limit: 10 })
// => [['posts', 'get'], {
// input: { limit: 10 },
// type: 'infinite',
// infinite: { initialPageParam: null }
// }]Cache Identity
Path parameters
Path parameters are stored separately from request input and retain their path position and application order. A query field cannot overwrite path identity, and sequential parameters with the same name remain distinct.
Request headers
Per-request headers are included in the input part of the query key. Different tenant, authorization, locale, or other response-varying headers therefore create separate cache entries.
Header values are visible in TanStack Query Devtools and can be serialized by dehydration or persistence. Treat raw secrets in per-request headers as an explicit cache-identity trade-off, and only use them when that exposure is understood and controlled.
Wrapped input migration
An explicit { query: value, headers: undefined } wrapper now uses
{ query: value, headers: {} } in its cache key. The request still receives no
per-request headers. This separates its cache entry from direct
{ query: value } input, which sends a query parameter named query.
All query and infinite-query option, key, and filter helpers use this normalization.
When upgrading, discard all old persisted entries for affected route/input
combinations, including entries whose keys look like direct input, or reset the
persisted cache. Old serialized keys cannot distinguish wrapper requests from
direct requests. Regenerate manually stored keys with the helpers.
Existing direct-input keys and wrappers with header values are unchanged.
Filters retain TanStack Query's partial
matching, so a direct-input filter can still match wrappers with the same
query value, and an empty-headers wrapper filter can match wrappers with
additional headers.
skipToken
skipToken is supported by queryOptions() and infiniteQueryOptions() to create disabled queries. It is not valid input for key-only helpers: queryKey(skipToken) and infiniteQueryKey(skipToken) throw an error.
Available Methods
queryKey()
Returns the query key for a specific route. Optionally accepts input for more specific targeting:
eden.users.get.queryKey() // all users queries
eden.users.get.queryKey({ role: 'admin' }) // specific input
eden.users({ id: '1' }).get.queryKey() // specific path paramsqueryFilter()
Returns a filter object (including queryKey) for use with TanStack Query's filter-based methods:
eden.users.get.queryFilter()
eden.users.get.queryFilter({ role: 'admin' })
eden.users.get.queryFilter(undefined, { exact: true, stale: true })queryKey() generates keys with type: "query", which only matches regular queries. By default, queryFilter() uses type: "any" to match both regular and infinite queries. With exact: true, it generates the same key as queryKey(input) and matches only that regular query. Use infiniteQueryFilter for exact infinite-query matching.
infiniteQueryKey()
Returns the query key for infinite queries:
eden.posts.get.infiniteQueryKey()
eden.posts.get.infiniteQueryKey({ limit: 10 })
eden.posts.get.infiniteQueryKey({ limit: 10 }, { initialCursor: 'start' })The initial cursor is part of an exact infinite-query key. The helper defaults
it to null for optional cursor routes, matching infiniteQueryOptions().
Required cursor routes must pass the same explicit non-null initialCursor to
both helpers.
infiniteQueryFilter()
Returns a filter object for infinite queries:
eden.posts.get.infiniteQueryFilter()
eden.posts.get.infiniteQueryFilter({ category: 'tech' })
eden.posts.get.infiniteQueryFilter(
{ category: 'tech' },
{ initialCursor: 'start' }
)Infinite-query filters match all initial-cursor variants by default. Pass
initialCursor in the filter options to target one variant. With the default
hash, cursor values are compared as complete values rather than partial object
or array prefixes, while route input keeps TanStack Query's normal partial
matching. A custom queryKeyHashFn defines cursor identity just as it does for
cache entries. On optional cursor routes, exact: true uses the default null
cursor when initialCursor is omitted. Required cursor routes allow an omitted
cursor only for broad filters with exact absent or false; exact and
cursor-specific filters require an explicit non-null cursor.
mutationKey()
Returns the mutation key for a mutation route:
eden.users.post.mutationKey()
// => [['users', 'post']]Cache Operations
invalidateQueries
Mark queries as stale and trigger a refetch:
const queryClient = useQueryClient()
const eden = useEden()
// Invalidate all users queries (partial matching)
queryClient.invalidateQueries({
queryKey: eden.users.get.queryKey()
})
// Invalidate with queryFilter
queryClient.invalidateQueries(
eden.users.get.queryFilter()
)
// Invalidate a specific user
queryClient.invalidateQueries({
queryKey: eden.users({ id: '1' }).get.queryKey()
})cancelQueries
Cancel in-flight queries (useful for optimistic updates):
await queryClient.cancelQueries({
queryKey: eden.todos.get.queryKey()
})getQueryData / setQueryData
Read and write the cache directly:
// Read cached data
const user = queryClient.getQueryData(
eden.users({ id: '1' }).get.queryKey()
)
// Write to cache
queryClient.setQueryData(
eden.users({ id: '1' }).get.queryKey(),
{ id: '1', name: 'Updated Name', email: 'updated@example.com' }
)removeQueries
Remove queries from the cache entirely:
queryClient.removeQueries({
queryKey: eden.users({ id: '1' }).get.queryKey()
})Partial Matching
TanStack Query uses partial matching for query keys. A key without input matches all queries for that route:
// Invalidates ALL users queries, regardless of input
queryClient.invalidateQueries({
queryKey: eden.users.get.queryKey()
})
// Only invalidates the specific query with role='admin'
queryClient.invalidateQueries({
queryKey: eden.users.get.queryKey({ role: 'admin' })
})This is useful for broad invalidation after mutations:
const createUser = useMutation({
...eden.users.post.mutationOptions(),
onSuccess: () => {
// Invalidates all users queries (list, filtered, by id, etc.)
queryClient.invalidateQueries({
queryKey: eden.users.get.queryKey()
})
},
})For more details on how partial matching works, see the TanStack Query docs on query invalidation.