Доки по разработке
This project is maintained by teniryte
queryFn отвечает только за получение данных. Всё, что связано с идентичностью кеша, живёт в queryKey, а всё, что связано с отображением, лучше выносить в select или UI.
queryFn получает QueryFunctionContext. На практике важнее всего четыре поля:
queryKeysignalmetapageParam для infinite queryimport type { QueryFunctionContext } from '@tanstack/react-query'
async function fetchTodoList({
queryKey,
signal,
}: QueryFunctionContext<readonly ['todos', { status: string }]>) {
const [, filters] = queryKey
const response = await fetch(`/api/todos?status=${filters.status}`, { signal })
if (!response.ok) {
throw new Error('Failed to load todos')
}
return response.json() as Promise<Todo[]>
}
signalОфициальная рекомендация: если ваш клиент поддерживает AbortSignal, используйте его.
const query = useQuery({
queryKey: ['todos'],
queryFn: ({ signal }) =>
fetch('/api/todos', { signal }).then((res) => res.json()),
})
Для axios >= 0.22:
import axios from 'axios'
useQuery({
queryKey: ['todos'],
queryFn: ({ signal }) => axios.get('/api/todos', { signal }),
})
По умолчанию, если компонент размонтировался до завершения запроса, промис не обязательно будет отменён. Но если вы реально используете signal, то:
Это особенно важно при:
Правильный подход:
async function fetchUser(id: string) {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error('Failed to load user')
return res.json() as Promise<User>
}
Неправильный подход:
null вместо ошибкиconsole.error и продолжать как будто всё хорошоdataTanStack Query ожидает, что неуспешный запрос бросает исключение.
Если API ненадёжное, валидируйте ответ прямо в queryFn.
const todosSchema = z.array(
z.object({
id: z.string(),
title: z.string(),
completed: z.boolean(),
}),
)
async function fetchTodos({ signal }: QueryFunctionContext) {
const res = await fetch('/api/todos', { signal })
if (!res.ok) throw new Error('Failed to load todos')
const json = await res.json()
return todosSchema.parse(json)
}
Это лучше, чем пропускать повреждённые данные дальше в UI.
metameta удобно использовать для служебного контекста:
const query = useQuery({
queryKey: ['reports', reportId],
queryFn: ({ meta, signal }) =>
fetchReport(reportId, { signal, source: meta?.source as string }),
meta: { source: 'dashboard' },
})
Подходит для:
Плохая идея:
queryFnХорошая идея:
queryFn возвращает серверный ответselect строит нужный срезconst query = useQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
select: (projects) => projects.filter((project) => project.active),
})
pageParamimport { infiniteQueryOptions } from '@tanstack/react-query'
export function messagesInfiniteOptions(chatId: string) {
return infiniteQueryOptions({
queryKey: ['messages', chatId],
initialPageParam: null as string | null,
queryFn: ({ pageParam, signal }) =>
fetchMessages({ chatId, cursor: pageParam, signal }),
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
})
}
pageParam существует только внутри infinite query. Не пытайтесь моделировать его вручную в обычном useQuery.
Лучший паттерн:
export function projectOptions(id: string) {
return queryOptions({
queryKey: ['projects', 'detail', id],
queryFn: ({ signal }) => api.projects.get(id, { signal }),
})
}
Тогда один и тот же набор опций можно использовать:
signal.queryKey.select, а не в queryFn.