# Tanstack Query Intent Core Design Query Keys And Options

> Use this when designing queryKey arrays, queryFn inputs, queryOptions, infiniteQueryOptions, mutationOptions, skipToken, key factories, or TypeScript inference for TanStack Query reads and writes.

- Skill: `lukasa1993/tanstack-query-intent-core-design-query-keys-and-options` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add lukasa1993/tanstack-query-intent-core-design-query-keys-and-options`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lukasa1993/tanstack-query-intent-core-design-query-keys-and-options/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: MIT
- Author: lukasa1993 (https://skillmd.com/u/lukasa1993)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lukasa1993/tanstack-query-intent-core-design-query-keys-and-options

---


## Setup

```ts
import { queryOptions } from '@tanstack/react-query'

export function todoOptions(todoId: string) {
  return queryOptions({
    queryKey: ['todo', todoId],
    queryFn: async () => ({ id: todoId, title: 'Ship skills' }),
    staleTime: 60_000,
  })
}
```

## Core Patterns

### Put every query variable in the key

```ts
import { queryOptions } from '@tanstack/react-query'

export function projectsOptions(teamId: string, page: number) {
  return queryOptions({
    queryKey: ['projects', teamId, page],
    queryFn: async () => ({ teamId, page, items: [] as Array<{ id: string }> }),
  })
}
```

### Share one options factory

```ts
import { QueryClient, useQuery, queryOptions } from '@tanstack/react-query'

export const queryClient = new QueryClient()

export function userOptions(userId: string) {
  return queryOptions({
    queryKey: ['user', userId],
    queryFn: async () => ({ id: userId, name: 'Tanner' }),
  })
}

export function useUser(userId: string) {
  return useQuery(userOptions(userId))
}

export function preloadUser(userId: string) {
  return queryClient.ensureQueryData(userOptions(userId))
}
```

### Use skipToken for typesafe absence

```ts
import { skipToken, useQuery } from '@tanstack/react-query'

export function useMaybeTodo(todoId: string | undefined) {
  return useQuery({
    queryKey: ['todo', todoId],
    queryFn: todoId ? async () => ({ id: todoId }) : skipToken,
  })
}
```

## Common Mistakes

### CRITICAL Missing variable in key

Wrong:

```ts
import { useQuery } from '@tanstack/react-query'

export function useProject(teamId: string) {
  return useQuery({
    queryKey: ['project'],
    queryFn: async () => ({ teamId }),
  })
}
```

Correct:

```ts
import { useQuery } from '@tanstack/react-query'

export function useProject(teamId: string) {
  return useQuery({
    queryKey: ['project', teamId],
    queryFn: async () => ({ teamId }),
  })
}
```

Query keys define cache identity; missing variables merge distinct data.

Source: TanStack/query:docs/framework/react/guides/query-keys.md

### HIGH Key and queryFn drift

Wrong:

```ts
import { useQuery } from '@tanstack/react-query'

export function useTodo(todoId: string) {
  return useQuery({ queryKey: ['todo'], queryFn: async () => ({ id: todoId }) })
}
```

Correct:

```ts
import { queryOptions, useQuery } from '@tanstack/react-query'

function todoOptions(todoId: string) {
  return queryOptions({
    queryKey: ['todo', todoId],
    queryFn: async () => ({ id: todoId }),
  })
}

export function useTodo(todoId: string) {
  return useQuery(todoOptions(todoId))
}
```

Options factories keep identity, fetch behavior, and inference together across hooks and prefetches.

Source: TanStack/query:docs/eslint/prefer-query-options.md

### HIGH skipToken inside suspense query

Wrong:

```ts
import { skipToken, useSuspenseQuery } from '@tanstack/react-query'

export function useTodo(todoId: string | undefined) {
  return useSuspenseQuery({
    queryKey: ['todo', todoId],
    queryFn: todoId ? async () => ({ id: todoId }) : skipToken,
  })
}
```

Correct:

```ts
import { skipToken, useQuery } from '@tanstack/react-query'

export function useTodo(todoId: string | undefined) {
  return useQuery({
    queryKey: ['todo', todoId],
    queryFn: todoId ? async () => ({ id: todoId }) : skipToken,
  })
}
```

Suspense queries require a guaranteed query function and cannot be conditionally disabled.

Source: TanStack/query:docs/framework/react/guides/suspense.md

See also: `compositions/enforce-query-best-practices-with-eslint` for rules that enforce key and options mistakes.

