Структура query functions

Query function в TanStack Query — это центральный элемент, отвечающий за получение данных. Именно она определяет, как, откуда и в каком формате данные попадают в кэш библиотеки. Несмотря на внешнюю простоту, структура query function имеет несколько важных нюансов, напрямую влияющих на предсказуемость кэширования, обработку ошибок, повторные запросы и масштабируемость приложения.

Базовая форма query function

В своей основе query function — это обычная асинхронная функция, которая возвращает данные:

const fetchUsers = async () => {
  const response = await fetch('/api/users');
  if (!response.ok) {
    throw new Error('Ошибка загрузки пользователей');
  }
  return response.json();
};

Эта функция передаётся в queryFn внутри useQuery:

import { useQuery } fr om '@tanstack/react-query';

const usersQuery = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
});

Ключевой момент: TanStack Query не требует специального формата возврата. Достаточно вернуть значение или Promise, который его резолвит.

Контракт query function

Query function в TanStack Query подчиняется простому контракту:

  1. Возвращает данные (sync или async).
  2. Бросает исключение при ошибке.
  3. Не занимается кэшированием — только получением данных.
  4. Не зависит от React-окружения.

Это разделение ответственности критично: TanStack Query управляет состоянием, а query function — только I/O логикой.

Параметры query function

TanStack Query передаёт в query function один аргумент — объект с контекстом запроса:

const fetchUserById = async ({ queryKey }) => {
  const [, id] = queryKey;

  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error('Ошибка загрузки пользователя');
  }

  return response.json();
};

Использование:

useQuery({
  queryKey: ['user', userId],
  queryFn: fetchUserById
});

Содержимое контекста

Контекст query function может включать:

  • queryKey — ключ запроса
  • signal — AbortSignal для отмены запроса
  • meta — пользовательские метаданные

Пример использования signal:

const fetchUsers = async ({ signal }) => {
  const response = await fetch('/api/users', { signal });
  if (!response.ok) {
    throw new Error('Ошибка загрузки');
  }
  return response.json();
};

Это особенно важно для отмены запросов при смене компонентов или ключей.

Использование queryKey внутри query function

queryKey — это источник параметров для запроса. TanStack Query не передаёт отдельные аргументы, поэтому вся параметризация строится через структуру ключа.

Пример:

useQuery({
  queryKey: ['posts', { page: 1, lim it: 10 }],
  queryFn: fetchPosts
});

Query function:

const fetchPosts = async ({ queryKey }) => {
  const [, params] = queryKey;

  const response = await fetch(
    `/api/posts?page=${params.page}&limit=${params.limit}`
  );

  if (!response.ok) {
    throw new Error('Ошибка загрузки постов');
  }

  return response.json();
};

Детерминированность query function

Одно из ключевых требований — детерминированность относительно queryKey.

Для одного и того же queryKey query function должна возвращать одинаковый результат (с точки зрения структуры данных), иначе нарушается логика кэширования.

Нарушения включают:

  • использование случайных значений (Math.random)
  • использование текущего времени без привязки к ключу
  • скрытые внешние состояния

Корректный подход:

const fetchFeed = async ({ queryKey }) => {
  const [, timestamp] = queryKey;

  const response = await fetch(`/api/feed?ts=${timestamp}`);
  return response.json();
};

Ошибки в query function

Любое исключение в query function трактуется как ошибка запроса.

const fetchData = async () => {
  const res = await fetch('/api/data');

  if (!res.ok) {
    throw new Error(`HTTP error: ${res.status}`);
  }

  return res.json();
};

TanStack Query перехватывает такие ошибки и переводит query в состояние error.

Важно: возврат null или undefined не считается ошибкой. Ошибка должна быть именно через throw.

Синхронные и асинхронные функции

Query function может быть как синхронной, так и асинхронной:

const syncQueryFn = () => {
  return { value: 42 };
};

или

const asyncQueryFn = async () => {
  const res = await fetch('/api/value');
  return res.json();
};

Однако в реальных приложениях синхронные функции используются редко, так как основной сценарий — работа с сетью или I/O.

Поведение при повторных вызовах

Query function может вызываться несколько раз в следующих случаях:

  • монтирование компонента
  • фокус окна (refetchOnWindowFocus)
  • изменение queryKey
  • ручной вызов refetch
  • истечение staleTime

Поэтому query function должна быть идемпотентной с точки зрения побочных эффектов.

Нежелательно:

const fetchData = async () => {
  console.log('Запрос отправлен');
  return fetch('/api/data').then(r => r.json());
};

Логирование допустимо, но любые побочные изменения состояния вне запроса — нет.

AbortSignal и отмена запросов

TanStack Query передаёт signal, позволяющий отменять запросы при устаревании:

const fetchUsers = async ({ signal }) => {
  const res = await fetch('/api/users', { signal });
  return res.json();
};

При смене queryKey предыдущий запрос может быть автоматически отменён. Это снижает нагрузку на сеть и предотвращает гонки данных.

Query function как адаптер API

На практике query function часто выступает слоем адаптации между API и приложением:

const fetchUser = async ({ queryKey }) => {
  const [, id] = queryKey;

  const res = await fetch(`/api/users/${id}`);

  if (!res.ok) {
    throw new Error('User not found');
  }

  const data = await res.json();

  return {
    id: data.id,
    fullName: `${data.firstName} ${data.lastName}`,
    isActive: data.status === 'active'
  };
};

Такой подход позволяет:

  • нормализовать данные
  • скрыть структуру API
  • централизовать трансформации

Ошибки проектирования query functions

Частые проблемы:

Смешивание логики данных и UI

Query function не должна знать о UI-логике:

// плохо
const fetchUsers = async () => {
  return fetch('/api/users').then(r => r.json()).then(data =>
    data.map(u => ({ ...u, selected: false }))
  );
};

Состояние UI должно формироваться отдельно.

Дублирование параметров вне queryKey

// плохо
const fetchPosts = async (page) => {
  return fetch(`/api/posts?page=${page}`);
};

Правильно — через queryKey.

Побочные эффекты

// плохо
const fetchData = async () => {
  localStorage.setItem('lastFetch', Date.now());
  return fetch('/api/data');
};

Query function должна оставаться чистой относительно внешнего состояния.

Типизация структуры query function

В TypeScript структура query function может быть явно описана:

import { QueryFunctionContext } from '@tanstack/react-query';

type User = {
  id: number;
  name: string;
};

const fetchUser = async (
  context: QueryFunctionContext<['user', number]>
): Promise<User> => {
  const [, id] = context.queryKey;

  const res = await fetch(`/api/users/${id}`);
  return res.json();
};

Это усиливает предсказуемость и снижает риск ошибок при изменении queryKey.

Композиция query functions

Query functions могут быть переиспользуемыми и композиционными:

const createFetcher = (baseUrl) => {
  return async ({ queryKey }) => {
    const [, path] = queryKey;
    const res = await fetch(`${baseUrl}/${path}`);
    return res.json();
  };
};

const fetchApi = createFetcher('/api');

useQuery({
  queryKey: ['api', 'users'],
  queryFn: fetchApi
});

Такой подход удобен для:

  • работы с несколькими API
  • версионирования эндпоинтов
  • тестирования

Взаимосвязь query function и caching

Хотя query function не управляет кэшем напрямую, её структура влияет на:

  • уникальность данных в кэше (через queryKey)
  • стабильность результата
  • повторное использование данных

Неправильная структура query function часто приводит к:

  • дублированию запросов
  • некорректному кэш-хиту
  • постоянным refetch

Поэтому query function рассматривается не как изолированная функция, а как часть контрактной системы вместе с queryKey.