tanstack-query-v5

agent67 작성설치 1회아직 좋아요 없음2026년 10월 5일 업데이트카테고리: 기타

무엇을 하나요

>

설치를 누르면 이 항목이 AgentsRoom 데스크톱 앱에서 열립니다. 앱이 아직 설치되어 있지 않으면 다운로드 페이지로 이동합니다.

SKILL.md

---
name: tanstack-query-v5
description: >
---

# TanStack Query Reference

> **Version:** 5.x
> **Docs:** https://tanstack.com/query/v5

## Imports

```typescript
// Core hooks
import { useQuery, useMutation, useInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query'

// Utilities
import { queryOptions, skipToken } from '@tanstack/react-query'

// Client
import { QueryClient, QueryClientProvider, useQueryClient } from '@tanstack/react-query'

// DevTools (dev only)
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
```

## API Quick Reference

### useQuery

```typescript
const { data, error, isLoading, isError, isFetching, refetch } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 5 * 60 * 1000,
  enabled: true,
})
```

**Key Options:**

| Option | Default | Description |
|--------|---------|-------------|
| `queryKey` | Required | Unique array identifier |
| `queryFn` | Required | Function returning Promise |
| `enabled` | `true` | Auto-execute when true |
| `staleTime` | `0` | Milliseconds before stale |
| `gcTime` | `5 * 60 * 1000` | Cache retention after unmount |
| `retry` | `3` | Retry attempts on error |
| `refetchOnWindowFocus` | `true` | Refetch on window focus |
| `refetchOnMount` | `true` | Refetch on component mount |
| `select` | `undefined` | Transform returned data |

**Return Values:**

| Property | Description |
|----------|-------------|
| `data` | Latest successfully resolved data (or undefined) |
| `error` | Error object if failed (or null) |
| `status` | `'pending' \| 'error' \| 'success'` |
| `isLoading` | `isPending && isFetching` |
| `isPending` | No data available yet |
| `isError` | Query failed |
| `isSuccess` | Query successful |
| `isFetching` | Currently fetching (including background) |
| `refetch()` | Manual refetch trigger |

### useMutation

```typescript
const mutation = useMutation({
  mutationFn: (data) => api.create(data),
  onMutate: async (variables) => {
    await queryClient.cancelQueries({ queryKey: ['todos'] })
    const previous = queryClient.getQueryData(['todos'])
    queryClient.setQueryData(['todos'], (old) => [...old, variables])
    return { previous }
  },
  onError: (err, variables, context) => {
    queryClient.setQueryData(['todos'], context.previous)
  },
  onSuccess: (data, variables, context) => {
    // Handle success
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})

mutation.mutate(data)
await mutation.mutateAsync(data)
```

**Key Options:**

| Option | Description |
|--------|-------------|
| `mutationFn` | Required mutation function |
| `onMutate` | Before mutation executes (returns context) |
| `onSuccess` | On successful completion |
| `onError` | On failure |
| `onSettled` | After success or error |
| `retry` | Retry logic (default: 0) |

**Return Values:**

| Property | Description |
|----------|-------------|
| `mutate()` | Fire-and-forget trigger |
| `mutateAsync()` | Promise-based trigger |
| `data` | Last successful result |
| `error` | Error object if failed |
| `isPending` | Mutation in progress |
| `isSuccess` | Mutation succeeded |
| `isError` | Mutation failed |
| `reset()` | Reset to initial state |

### useInfiniteQuery

```typescript
const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
} = useInfiniteQuery({
  queryKey: ['todos'],
  queryFn: ({ pageParam }) => fetchTodos({ cursor: pageParam }),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
  getPreviousPageParam: (firstPage) => firstPage.prevCursor ?? undefined,
  maxPages: 10,
})

data.pages.map((page) => page.items)
```

### useSuspenseQuery

Data is never undefined. Requires Suspense boundary.

```typescript
const { data } = useSuspenseQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id),
})
```

### QueryClient Methods

```typescript
const queryClient = useQueryClient()

queryClient.getQueryData(['todos'])
queryClient.setQueryData(['todos'], newTodos)
queryClient.invalidateQueries({ queryKey: ['todos'] })
queryClient.refetchQueries({ queryKey: ['todos'] })
queryClient.cancelQueries({ queryKey: ['todos'] })
await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos })
queryClient.removeQueries({ queryKey: ['todos'] })
queryClient.clear()
```

## Gotchas & Version Caveats

### Query Keys

- Must be arrays at the top level
- Must be serializable with JSON.stringify
- Include ALL variables the queryFn depends on
- Object property order doesn't matter (deterministic hashing)
- Array order DOES matter

```typescript
// These are equivalent
['todos', { status, page }]
['todos', { page, status }]

// These are different
['todos', status, page]
['todos', page, status]
```

### Query States

Two separate state axes:

- **`status`**: Do we have data? (`pending | error | success`)
- **`fetchStatus`**: Is the query function running? (`fetching | paused | idle`)

A query can be `success` while `fetching` background updates.

### skipToken vs enabled

```typescript
// enabled: false - allows manual refetch
const { refetch } = useQuery({
  queryKey: ['todo', id],
  queryFn: () => fetchTodo(id!),
  enabled: !!id,
})

// skipToken - cleaner types but no manual refetch
const { data } = useQuery({
  queryKey: ['todo', id],
  queryFn: id ? () => fetchTodo(id) : skipToken,
})
```

**Important:** `refetch()` throws "Missing queryFn" error with `skipToken`.

### mutate vs mutateAsync

```typescript
// Bad - mutate() is fire-and-forget, doesn't wait
await mutation.mutate(data)

// Good - use mutateAsync for async/await
await mutation.mutateAsync(data)
```

### staleTime vs gcTime

- **`staleTime`**: How long data is considered fresh (default: 0)
- **`gcTime`**: How long unused data stays in cache (default: 5 minutes)

## Common Patterns

### queryOptions() Helper

Colocate configuration for reuse and type inference.

```typescript
export function todosQueryOptions(filters: Filters = {}) {
  return queryOptions({
    queryKey: ['todos', 'list', filters] as const,
    queryFn: (): Promise<TodosResponse> => api.todos.list(filters),
    staleTime: 30_000,
  })
}

// Component usage
const { data } = useQuery(todosQueryOptions(filters))

// Prefetch
queryClient.prefetchQuery(todosQueryOptions(filters))

// Cache access
const cached = queryClient.getQueryData(todosQueryOptions(filters).queryKey)
```

### Query Key Organization

Hierarchical keys enable prefix invalidation.

```typescript
['todos']                              // All todos
['todos', 'list', filters]             // Filtered list
['todos', id]                          // Single todo

// Invalidate all todos queries
queryClient.invalidateQueries({ queryKey: ['todos'] })

// Invalidate only specific todo
queryClient.invalidateQueries({ queryKey: ['todos', id], exact: true })
```

### Optimistic Updates

```typescript
// UI-based (simpler)
const { isPending, variables, mutate } = useMutation({
  mutationFn: updateTodo,
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] })
})
{isPending && <div style={{ opacity: 0.5 }}>{variables.title}</div>}

// Cache-based (multi-location updates)
const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    await queryClient.cancelQueries({ queryKey: ['todos'] })
    const previous = queryClient.getQueryData(['todos'])
    queryClient.setQueryData(['todos'], (old) =>
      old.map(t => t.id === newTodo.id ? newTodo : t)
    )
    return { previous }
  },
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previous)
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})
```

### Conditional Queries

```typescript
const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId!),
  enabled: !!userId,
})

// Dependent query
const { data: posts } = useQuery({
  queryKey: ['posts', user?.id],
  queryFn: () => fetchPosts(user!.id),
  enabled: !!user,
})
```

### Data Transformation with select

```typescript
const { data: titles } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  select: (todos) => todos.map(t => t.title),
})
// titles is string[], cache still stores Todo[]
```

### Background Refetching

```typescript
// Polling
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  refetchInterval: 30_000,
})

// Conditional polling
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  refetchInterval: (query) =>
    query.state.data?.shouldPoll ? 30_000 : false,
})
```

## Anti-Patterns

### Don't forget query key dependencies
```typescript
// Bad - won't refetch when userId changes
useQuery({ queryKey: ['todos'], queryFn: () => fetchUserTodos(userId) })

// Good
useQuery({ queryKey: ['todos', userId], queryFn: () => fetchUserTodos(userId) })
```

### Don't mutate cache directly
```typescript
// Bad - breaks reactivity
const todos = queryClient.getQueryData(['todos'])
todos.push(newTodo)

// Good
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
```

### Don't use refetchQueries instead of invalidateQueries
```typescript
// Bad - forces refetch even if no observers
queryClient.refetchQueries({ queryKey: ['todos'] })

// Good - marks stale, refetches only when needed
queryClient.invalidateQueries({ queryKey: ['todos'] })
```

### Don't skip onSettled for cleanup
```typescript
// Bad - doesn't refetch on error
useMutation({
  mutationFn: updateTodo,
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

// Good - always refetches
useMutation({
  mutationFn: updateTodo,
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
```

### Don't skip cancellation before optimistic updates
```typescript
// Bad - race condition
onMutate: async (newTodo) => {
  queryClient.setQueryData(['todos'], ...)
}

// Good
onMutate: async (newTodo) => {
  await queryClient.cancelQueries({ queryKey: ['todos'] })
  queryClient.setQueryData(['todos'], ...)
}
```

더 알아보기

DevOps 엔지니어콘텐츠 에디터프론트엔드 개발자영업 담당자백엔드 개발자코치·트레이너소프트웨어 아키텍트법무 담당QA 엔지니어재무·총무풀스택 개발자크리에이티브 디자이너모바일 개발자리서처보안 엔지니어인사·채용
프로젝트 매니저번역가Git 전문가데이터 분석가SEO 전문가회계 담당자로컬라이제이션 전문가홍보·PR브레인스토머운영·물류마케팅 전문가고객 지원프로젝트 어시스턴트구매 담당자커뮤니티 매니저개인 비서

AgentsRoom 다운로드

모든 AI 에이전트를, 모든 프로젝트에서, 하나의 창으로 실행하세요.

AgentsRoom 다운로드무료

컴패니언 앱: 이동 중에도 에이전트를 모니터링

Claude, Codex, Antigravity CLI 또는 다른 AI 공급자를 사용하세요.

확장 프로그램 설치
Chrome Web Store

버그와 요청을 공개 백로그로 바로 보내세요.

대화처럼 읽는 에이전트

에이전트마다 역할과 얼굴이 있고, 메신저 앱처럼 스레드를 읽을 수 있습니다.

대화 보기

AgentsRoom 대화 보기: AI 코딩 에이전트의 작업이 채팅 스레드로 표시된 화면
멀티 프로젝트
멀티 프로바이더
멀티 에이전트
실시간 상태
파일 diff & 커밋
모바일 앱
라이브 프리뷰
에이전트 팀
브라우저 자동화
백로그 기반 개발
프롬프트 라이브러리
스킬 라이브러리
모든 기능 보기