# Tanstack Query Intent Core Understand Query Internals A 8ab976f4

> Use this when explaining or debugging TanStack Query internals: QueryClient, QueryCache, MutationCache, Query, QueryObserver, active versus inactive queries, observer-level options, subscriptions, stale timers, and why loaders or imperative cache reads are not the same as observed queries.

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

---


## Mental Model

`QueryClient` owns the caches. `QueryCache` stores `Query` instances. Hooks and adapter APIs create `QueryObserver` instances that subscribe components to one query. Observer-level options like `select`, `staleTime`, polling, and tracked result access shape what each component sees.

An inactive query can exist in the cache without active observers. It can be read imperatively, hydrated, invalidated, or garbage collected, but it is not the same as a component actively observing query state.

## Core Patterns

### Use observers for UI reads

```tsx
function TodoPage({ id }: { id: string }) {
  const todo = useQuery({
    queryKey: ['todo', id],
    queryFn: () => fetchTodo(id),
  })

  if (todo.isPending) return <p>Loading...</p>
  if (todo.isError) return <p>{todo.error.message}</p>
  return <h1>{todo.data.title}</h1>
}
```

### Use QueryClient for cache orchestration

```ts
await queryClient.ensureQueryData(todoOptions(id))
queryClient.invalidateQueries({ queryKey: ['todo', id] })
queryClient.setQueryData(['todo', id], (old) => old && { ...old, title })
```

## Common Mistakes

### HIGH Treating cache presence as active usage

Wrong:

```tsx
const todo = queryClient.getQueryData(['todo', id])
return <TodoView todo={todo} />
```

Correct:

```tsx
const todo = useQuery(todoOptions(id))
return <TodoView todo={todo.data} />
```

UI reads should create observers so invalidation, refetch triggers, stale status, and garbage collection behave as expected.

Source: https://tkdodo.eu/blog/inside-react-query

### MEDIUM Expecting one query to have one option set

Wrong:

```ts
// assume staleTime is stored only on the Query
queryClient.getQueryCache().find({ queryKey })?.options.staleTime
```

Correct:

```ts
const query = queryClient.getQueryCache().find({ queryKey })
const observerStaleTimes = query?.observers.map(
  (observer) => observer.options.staleTime,
)
```

Several options are observer-level, so multiple components can observe the same query with different selectors or freshness behavior.

Source: https://tkdodo.eu/blog/automatic-query-invalidation-after-mutations

### MEDIUM Debugging without checking observer count

Wrong:

```ts
queryClient.invalidateQueries({ queryKey: ['todos'] })
// expect every cached todo query to refetch immediately
```

Correct:

```ts
queryClient.invalidateQueries({ queryKey: ['todos'], refetchType: 'active' })
```

Inactive queries are not automatically the same as mounted queries. Check observer count in devtools when invalidation or garbage collection looks surprising.

Source: https://tkdodo.eu/blog/inside-react-query

