Skip to main content
Query functions are the functions that actually fetch your data. They can be any function that returns a promise.

Basic Query Function

A query function must return a promise that resolves data or throws an error:
import { useQuery } from '@tanstack/react-query'

function useTodos() {
  return useQuery({
    queryKey: ['todos'],
    queryFn: async () => {
      const response = await fetch('https://api.example.com/todos')
      if (!response.ok) {
        throw new Error('Network response was not ok')
      }
      return response.json()
    },
  })
}
Query functions must throw errors instead of returning them for proper error handling.

Query Function Context

Query functions receive a context object with useful properties:
type QueryFunctionContext = {
  queryKey: QueryKey,      // The query key
  signal: AbortSignal,     // AbortSignal for cancellation
  meta: QueryMeta,         // Optional meta information
  client: QueryClient,     // The QueryClient instance
}

Using the Context

useQuery({
  queryKey: ['todo', todoId],
  queryFn: async ({ queryKey, signal }) => {
    // Destructure queryKey
    const [, id] = queryKey
    
    // Use signal for cancellation
    const response = await fetch(`/api/todos/${id}`, { signal })
    return response.json()
  },
})

Query Function Patterns

useQuery({
  queryKey: ['todos'],
  queryFn: async () => {
    const res = await fetch('/api/todos')
    return res.json()
  },
})

Handling Errors

Query functions should throw errors for failed requests:
const fetchTodos = async () => {
  const response = await fetch('/api/todos')
  
  if (!response.ok) {
    // Throw an error to trigger error state
    throw new Error(`HTTP error! status: ${response.status}`)
  }
  
  return response.json()
}

function Todos() {
  const { data, error, isError } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  if (isError) {
    return <div>Error: {error.message}</div>
  }
}
Do not return errors from query functions. Always throw them.

Query Function with Axios

import axios from 'axios'

const fetchTodos = async () => {
  // Axios automatically throws on non-2xx responses
  const { data } = await axios.get('/api/todos')
  return data
}

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

Query Function with Fetch API

Complete example with proper error handling:
const fetchTodo = async (id: number): Promise<Todo> => {
  const response = await fetch(`/api/todos/${id}`)
  
  if (!response.ok) {
    const error = await response.json()
    throw new Error(error.message || 'Failed to fetch todo')
  }
  
  return response.json()
}

function useTodo(id: number) {
  return useQuery({
    queryKey: ['todo', id],
    queryFn: () => fetchTodo(id),
  })
}

Cancellation with AbortSignal

Use the signal for request cancellation:
const fetchTodos = async ({ signal }: { signal: AbortSignal }) => {
  const response = await fetch('/api/todos', { signal })
  return response.json()
}

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})
When a query is cancelled (e.g., component unmounts), the AbortSignal is automatically triggered.

Dependent Data in Query Functions

Extract dependencies from the query key:
function useUserProjects(userId: number) {
  return useQuery({
    queryKey: ['projects', userId],
    queryFn: async ({ queryKey }) => {
      const [, userId] = queryKey
      const response = await fetch(`/api/users/${userId}/projects`)
      return response.json()
    },
  })
}

Parallel Requests in Query Function

Fetch multiple resources in a single query:
const fetchDashboardData = async () => {
  const [users, posts, comments] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/comments').then(r => r.json()),
  ])
  
  return { users, posts, comments }
}

useQuery({
  queryKey: ['dashboard'],
  queryFn: fetchDashboardData,
})

Default Query Function

Set a default query function for all queries:
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      queryFn: async ({ queryKey, signal }) => {
        const [url] = queryKey
        const response = await fetch(`${url}`, { signal })
        if (!response.ok) {
          throw new Error('Network error')
        }
        return response.json()
      },
    },
  },
})

// Now you can omit queryFn
useQuery({ queryKey: ['/api/todos'] })

TypeScript Query Functions

Type-safe query functions:
interface Todo {
  id: number
  title: string
  completed: boolean
}

const fetchTodo = async (id: number): Promise<Todo> => {
  const response = await fetch(`/api/todos/${id}`)
  if (!response.ok) throw new Error('Failed to fetch')
  return response.json()
}

function useTodo(id: number) {
  return useQuery({
    queryKey: ['todo', id],
    queryFn: () => fetchTodo(id),
  })
}

Query Functions with GraphQL

import { request, gql } from 'graphql-request'

const endpoint = 'https://api.example.com/graphql'

const fetchTodos = async () => {
  const query = gql`
    query {
      todos {
        id
        title
        completed
      }
    }
  `
  
  return request(endpoint, query)
}

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

Query Functions with tRPC

import { trpc } from './trpc'

function useTodos() {
  return useQuery({
    queryKey: ['todos'],
    queryFn: () => trpc.todos.list.query(),
  })
}

Paginated Query Function

interface TodosResponse {
  todos: Todo[]
  nextCursor?: number
}

const fetchTodos = async (page: number): Promise<TodosResponse> => {
  const response = await fetch(`/api/todos?page=${page}`)
  return response.json()
}

function useTodos(page: number) {
  return useQuery({
    queryKey: ['todos', page],
    queryFn: () => fetchTodos(page),
  })
}

Infinite Query Function

const fetchProjects = async ({ pageParam = 0 }) => {
  const response = await fetch(`/api/projects?cursor=${pageParam}`)
  return response.json()
}

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage) => firstPage.prevCursor,
})

Query Function Error Types

Type errors properly:
class ApiError extends Error {
  constructor(
    message: string,
    public statusCode: number,
    public data?: unknown
  ) {
    super(message)
    this.name = 'ApiError'
  }
}

const fetchTodo = async (id: number): Promise<Todo> => {
  const response = await fetch(`/api/todos/${id}`)
  
  if (!response.ok) {
    const errorData = await response.json()
    throw new ApiError(
      errorData.message,
      response.status,
      errorData
    )
  }
  
  return response.json()
}

function useTodo(id: number) {
  return useQuery<Todo, ApiError>({
    queryKey: ['todo', id],
    queryFn: () => fetchTodo(id),
  })
}

Retry Logic in Query Functions

Query functions automatically retry based on query options:
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  retry: 3, // Retry 3 times before failing
  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
})
The query function doesn’t need to implement retry logic - TanStack Query handles it automatically.

Query Function with Authentication

const fetchWithAuth = async (url: string, token: string) => {
  const response = await fetch(url, {
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
  })
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`)
  }
  
  return response.json()
}

function useTodos(token: string) {
  return useQuery({
    queryKey: ['todos', token],
    queryFn: () => fetchWithAuth('/api/todos', token),
    enabled: !!token,
  })
}

Transforming Data in Query Functions

interface TodoDTO {
  id: number
  title: string
  completed_at: string | null
}

interface Todo {
  id: number
  title: string
  completed: boolean
}

const fetchTodos = async (): Promise<Todo[]> => {
  const response = await fetch('/api/todos')
  const dtos: TodoDTO[] = await response.json()
  
  // Transform DTOs to domain models
  return dtos.map(dto => ({
    id: dto.id,
    title: dto.title,
    completed: dto.completed_at !== null,
  }))
}

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})
Transform data in the query function for consistent structure, or use the select option for view-specific transformations.

Query Function Best Practices

1

Always Return Promises

Query functions must return a promise.
// Good
queryFn: async () => { /* ... */ }
queryFn: () => fetch('/api/todos')

// Bad
queryFn: () => data // Not a promise
2

Throw Errors

Throw errors instead of returning them.
// Good
if (!response.ok) throw new Error()

// Bad
if (!response.ok) return { error: true }
3

Use AbortSignal

Support cancellation with the signal.
queryFn: ({ signal }) => fetch('/api/todos', { signal })
4

Extract from Query Key

Get parameters from the query key for consistency.
queryFn: ({ queryKey }) => {
  const [, id] = queryKey
  return fetchTodo(id)
}

Build docs developers (and LLMs) love