Синтаксис и параметры

TanStack Query предоставляет набор хуков, объектов конфигурации и API-клиентов для управления серверным состоянием. Основой синтаксиса библиотеки являются:

  • useQuery
  • useMutation
  • queryKey
  • queryFn
  • QueryClient
  • методы invalidateQueries, setQueryData, prefetchQuery
  • объект параметров конфигурации

Библиотека строится вокруг декларативного описания запросов. Вместо ручного управления состоянием загрузки, ошибками и кешированием разработчик описывает:

  • что нужно загрузить;
  • по какому ключу хранить данные;
  • как долго считать их актуальными;
  • когда обновлять;
  • как реагировать на ошибки.

Синтаксис useQuery

Базовый синтаксис:

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

Здесь используются два обязательных параметра:

Параметр Назначение
queryKey Уникальный ключ кеша
queryFn Функция загрузки данных

Возвращаемое значение содержит объект состояния запроса:

const {
    data,
    error,
    isLoading,
    isError,
    isSuccess,
    refetch
} = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
})

Параметр queryKey

Назначение queryKey

queryKey — идентификатор кеша запроса.

TanStack Query использует ключ для:

  • хранения результата;
  • поиска данных;
  • повторного использования кеша;
  • инвалидации;
  • автоматического обновления;
  • синхронизации компонентов.

Простейший queryKey

queryKey: ['users']

Ключ всегда рекомендуется задавать массивом.

Даже если используется одно значение, массив обеспечивает:

  • масштабируемость;
  • предсказуемость;
  • совместимость со сложными ключами.

Динамические queryKey

Очень часто ключ зависит от параметров:

queryKey: ['user', userId]

Пример:

const { data } = useQuery({
    queryKey: ['user', 15],
    queryFn: () => fetchUser(15)
})

TanStack Query создаст отдельный кеш для каждого userId.


Сложные queryKey

Допускается использование объектов:

queryKey: [
    'posts',
    {
        page: 1,
        limit: 20,
        sort: 'date'
    }
]

Это особенно полезно при:

  • пагинации;
  • фильтрации;
  • сортировке;
  • поиске;
  • серверных таблицах.

Важность стабильности queryKey

Ключ должен быть:

  • детерминированным;
  • сериализуемым;
  • стабильным.

Нежелательный вариант:

queryKey: ['users', new Date()]

Такой ключ будет постоянно изменяться.

Корректный вариант:

queryKey: ['users', dateString]

Параметр queryFn

Назначение queryFn

queryFn — функция, которая выполняет запрос.

Пример:

const fetchUsers = async () => {
    const response = await fetch('/api/users')

    if (!response.ok) {
        throw new Error('Ошибка загрузки')
    }

    return response.json()
}

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

useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
})

Асинхронность queryFn

Функция должна возвращать Promise.

Допустимы:

queryFn: async () => {
    return await fetchData()
}

или:

queryFn: () => fetchData()

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

TanStack Query передаёт объект контекста:

useQuery({
    queryKey: ['user', userId],
    queryFn: ({ queryKey }) => {
        const [, id] = queryKey

        return fetchUser(id)
    }
})

Это позволяет строить универсальные функции.


Синтаксис queryFn context

Объект содержит:

Поле Назначение
queryKey Ключ запроса
signal AbortSignal
meta Дополнительные данные

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

TanStack Query поддерживает отмену запросов.

Пример:

useQuery({
    queryKey: ['users'],
    queryFn: async ({ signal }) => {
        const response = await fetch('/api/users', {
            signal
        })

        return response.json()
    }
})

Если компонент размонтируется или запрос станет неактуальным, библиотека отменит fetch.


Параметр enabled

enabled управляет автоматическим запуском запроса.

Отключённый запрос

useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    enabled: false
})

Запрос не выполнится автоматически.


Условный запуск

Одна из самых частых схем:

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

Запрос выполнится только после появления userId.


Параметр staleTime

Назначение staleTime

staleTime определяет время актуальности данных.

useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    staleTime: 60000
})

В течение 60 секунд данные считаются свежими.


Поведение staleTime

Пока данные свежие:

  • повторный рендер не вызовет новый запрос;
  • переключение вкладок не вызовет refetch;
  • повторное монтирование использует кеш.

После истечения времени данные становятся stale.


Значения staleTime

Немедшее устаревание

staleTime: 0

Поведение по умолчанию.


Долгое кеширование

staleTime: 1000 * 60 * 5

Пять минут свежих данных.


Бесконечная актуальность

staleTime: Infinity

Автоматическое обновление отключается.


Параметр gcTime

Ранее использовался cacheTime.

В современных версиях применяется gcTime.

useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    gcTime: 1000 * 60 * 10
})

Назначение gcTime

Параметр определяет:

  • как долго хранить неиспользуемый кеш;
  • когда удалять данные из памяти.

Если запрос больше никем не используется, запускается таймер garbage collection.


Параметр refetchOnWindowFocus

Автоматическое обновление при возврате на вкладку:

useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    refetchOnWindowFocus: true
})

Варианты refetchOnWindowFocus

Значение Поведение
true Обновлять stale-запросы
false Не обновлять
"always" Всегда обновлять

Параметр refetchInterval

Периодический polling:

useQuery({
    queryKey: ['stats'],
    queryFn: fetchStats,
    refetchInterval: 5000
})

Запрос выполняется каждые 5 секунд.


Фоновое обновление

Дополнительно:

refetchIntervalInBackground: true

Обновление продолжится даже в неактивной вкладке.


Параметр retry

Управление повторными попытками:

useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    retry: 3
})

Варианты retry

Отключение

retry: false

Количество попыток

retry: 5

Функция

retry: (failureCount, error) => {
    if (error.status === 404) {
        return false
    }

    return failureCount < 3
}

Параметр retryDelay

Интервал между попытками:

retryDelay: 2000

Или функция:

retryDelay: attempt => {
    return Math.min(1000 * 2 ** attempt, 30000)
}

Параметр select

select преобразует данные.

useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    select: users => {
        return users.map(user => user.name)
    }
})

Особенности select

select:

  • не изменяет оригинальный кеш;
  • создаёт производное представление;
  • выполняется только при изменении данных.

Параметр placeholderData

Временные данные до загрузки:

useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    placeholderData: []
})

Отличие placeholderData от initialData

placeholderData

  • временное значение;
  • не попадает в кеш;
  • заменяется после запроса.

initialData

initialData: []
  • записывается в кеш;
  • считается реальными данными;
  • участвует в логике staleTime.

Параметр initialDataUpdatedAt

Позволяет указать время актуальности initialData.

initialDataUpdatedAt: Date.now()

Параметр keepPreviousData

Используется при пагинации.

useQuery({
    queryKey: ['posts', page],
    queryFn: () => fetchPosts(page),
    keepPreviousData: true
})

Поведение keepPreviousData

При смене страницы:

  • старые данные остаются видимыми;
  • исчезает UI-мерцание;
  • интерфейс работает плавнее.

Параметр meta

Дополнительные данные запроса:

useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    meta: {
        requiresAuth: true
    }
})

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

meta часто применяется:

  • в middleware;
  • логировании;
  • DevTools;
  • системах аналитики;
  • кастомных fetch-обёртках.

Синтаксис useMutation

useMutation используется для операций изменения данных.

Базовый синтаксис:

const mutation = useMutation({
    mutationFn: createUser
})

Возвращаемые поля useMutation

const {
    mutate,
    mutateAsync,
    data,
    error,
    isPending,
    isSuccess
} = useMutation({
    mutationFn: createUser
})

Параметр mutationFn

Функция изменения данных:

const createUser = async user => {
    const response = await fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(user)
    })

    return response.json()
}

Вызов mutate

mutate({
    name: 'Alex'
})

Колбеки useMutation

onSuccess

useMutation({
    mutationFn: createUser,
    onSuccess: () => {
        console.log('Успех')
    }
})

onError

onError: error => {
    console.error(error)
}

onSettled

Срабатывает всегда:

onSettled: () => {
    console.log('Завершено')
}

invalidateQueries

После мутации часто требуется обновление кеша.

const queryClient = useQueryClient()

useMutation({
    mutationFn: createUser,
    onSuccess: () => {
        queryClient.invalidateQueries({
            queryKey: ['users']
        })
    }
})

Синтаксис QueryClient

Создание клиента:

const queryClient = new QueryClient()

Глобальные параметры

const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            staleTime: 60000,
            retry: 2
        }
    }
})

defaultOptions

Поддерживаются:

Раздел Назначение
queries Настройки useQuery
mutations Настройки useMutation

Методы QueryClient

invalidateQueries

Инвалидация кеша:

queryClient.invalidateQueries({
    queryKey: ['users']
})

refetchQueries

Принудительное обновление:

queryClient.refetchQueries({
    queryKey: ['users']
})

removeQueries

Удаление кеша:

queryClient.removeQueries({
    queryKey: ['users']
})

setQueryData

Ручное обновление кеша:

queryClient.setQueryData(
    ['user', user.id],
    user
)

getQueryData

Получение данных из кеша:

const user = queryClient.getQueryData([
    'user',
    userId
])

Синтаксис invalidateQueries

Поддерживает фильтрацию:

queryClient.invalidateQueries({
    queryKey: ['posts']
})

exact

Точное совпадение:

queryClient.invalidateQueries({
    queryKey: ['posts'],
    exact: true
})

invalidateQueries по префиксу

Без exact:

['posts']

совпадает с:

['posts', 1]
['posts', 2]
['posts', 'featured']

Синтаксис prefetchQuery

Предварительная загрузка:

await queryClient.prefetchQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
})

ensureQueryData

Комбинация кеша и загрузки:

await queryClient.ensureQueryData({
    queryKey: ['users'],
    queryFn: fetchUsers
})

Типичный полный синтаксис useQuery

const query = useQuery({
    queryKey: ['posts', page],
    queryFn: () => fetchPosts(page),

    enabled: true,

    staleTime: 60000,
    gcTime: 300000,

    retry: 3,
    retryDelay: 1000,

    refetchOnWindowFocus: true,
    refetchOnReconnect: true,
    refetchOnMount: true,

    refetchInterval: false,

    select: data => data.items,

    placeholderData: previousData,

    meta: {
        analytics: true
    }
})

refetchOnReconnect

Повторный запрос после восстановления сети:

refetchOnReconnect: true

refetchOnMount

Поведение при повторном монтировании:

refetchOnMount: true

Варианты:

Значение Поведение
true Обновлять stale-запрос
false Не обновлять
"always" Всегда обновлять

networkMode

Режим работы сети:

networkMode: 'online'

Варианты networkMode

Значение Назначение
online Стандартное поведение
always Игнорировать offline
offlineFirst Поддержка offline-first

notifyOnChangeProps

Оптимизация ререндеров:

notifyOnChangeProps: ['data', 'error']

Компонент будет обновляться только при изменении указанных полей.


structuralSharing

Оптимизация ссылочной целостности:

structuralSharing: true

Позволяет уменьшать лишние рендеры за счёт повторного использования неизменённых частей объекта.


throwOnError

Проброс ошибок в Error Boundary:

throwOnError: true

suspense

Интеграция с React Suspense:

suspense: true

В этом режиме TanStack Query выбрасывает Promise во время загрузки.


useErrorBoundary

Передача ошибок в React Error Boundary:

useErrorBoundary: true

Комбинация параметров в реальном проекте

const usersQuery = useQuery({
    queryKey: ['users', filters],

    queryFn: ({ queryKey }) => {
        const [, currentFilters] = queryKey

        return fetchUsers(currentFilters)
    },

    staleTime: 1000 * 60 * 5,

    gcTime: 1000 * 60 * 30,

    retry: 2,

    refetchOnWindowFocus: false,

    keepPreviousData: true,

    select: response => {
        return response.data
    },

    enabled: filters.ready
})

Основные принципы построения синтаксиса TanStack Query

Декларативность

Разработчик описывает состояние запроса, а не управляет им вручную.


Конфигурационность

Практически любое поведение меняется параметрами:

  • кеширование;
  • обновление;
  • retry;
  • polling;
  • stale logic;
  • refetch logic;
  • синхронизация.

Композиционность

Большинство параметров можно комбинировать:

useQuery({
    queryKey: ['stats'],
    queryFn: fetchStats,

    staleTime: 60000,

    retry: 5,

    refetchInterval: 10000,

    refetchOnWindowFocus: false
})

Типичные ошибки в синтаксисе

Нестабильный queryKey

queryKey: [Math.random()]

Создаёт бесконечные новые кеши.


Отсутствие queryKey-зависимостей

Плохой вариант:

queryKey: ['user']
queryFn: () => fetchUser(userId)

Корректный:

queryKey: ['user', userId]

Изменение данных внутри select

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

select: data => {
    data.users.push(newUser)

    return data
}

Неправильный enabled

Опасный вариант:

enabled: userId

Если userId = 0, запрос не выполнится.

Корректнее:

enabled: userId !== undefined