Синтаксис и базовое использование

Библиотека TanStack Query предназначена для управления асинхронным состоянием приложения. Основная задача — получение, кэширование, синхронизация и обновление серверных данных.

В обычном React-приложении работа с API часто приводит к следующим проблемам:

  • постоянное дублирование useEffect
  • ручное управление loading
  • ручное управление ошибками
  • необходимость самостоятельно кэшировать данные
  • повторные запросы при переходах
  • сложная синхронизация данных между компонентами

Типичный код без TanStack Query:

import { useEffect, useState } from 'react'

function Users() {
    const [users, setUsers] = useState([])
    const [loading, setLoading] = useState(true)
    const [error, setError] = useState(null)

    useEffect(() => {
        fetch('/api/users')
            .then(res => res.json())
            .then(data => {
                setUsers(data)
                setLoading(false)
            })
            .catch(err => {
                setError(err)
                setLoading(false)
            })
    }, [])

    if (loading) {
        return <div>Загрузка...</div>
    }

    if (error) {
        return <div>Ошибка</div>
    }

    return (
        <ul>
            {users.map(user => (
                <li key={user.id}>
                    {user.name}
                </li>
            ))}
        </ul>
    )
}

При росте приложения подобный код начинает усложняться. Появляются:

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

TanStack Query решает эти задачи централизованно.


Установка библиотеки

Для React используется пакет:

npm install @tanstack/react-query

Для работы Devtools:

npm install @tanstack/react-query-devtools

Создание QueryClient

Центральным объектом библиотеки является QueryClient.

Он отвечает за:

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

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

import { QueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient()

Подключение QueryClientProvider

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

import React from 'react'
import ReactDOM from 'react-dom/client'

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

import App from './App'

const queryClient = new QueryClient()

ReactDOM.createRoot(document.getElementById('root')).render(
    <QueryClientProvider client={queryClient}>
        <App />
    </QueryClientProvider>
)

QueryClientProvider делает клиент доступным во всем дереве компонентов.


Первый запрос через useQuery

Основной хук библиотеки — useQuery.

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

useQuery({
    queryKey,
    queryFn
})

queryKey

Уникальный ключ запроса.

queryFn

Функция получения данных.


Простейший пример

import { useQuery } from '@tanstack/react-query'

function Users() {
    const {
        data,
        isLoading,
        error
    } = useQuery({
        queryKey: ['users'],
        queryFn: async () => {
            const response = await fetch('/api/users')

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

            return response.json()
        }
    })

    if (isLoading) {
        return <div>Загрузка...</div>
    }

    if (error) {
        return <div>Ошибка</div>
    }

    return (
        <ul>
            {data.map(user => (
                <li key={user.id}>
                    {user.name}
                </li>
            ))}
        </ul>
    )
}

Что происходит внутри useQuery

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

  1. TanStack Query проверяет наличие данных в кэше
  2. Если данных нет — запускается queryFn
  3. Результат сохраняется в кэш
  4. Компонент получает данные
  5. При повторном использовании запроса данные берутся из кэша

Структура объекта useQuery

useQuery возвращает большой объект состояния.

Наиболее используемые поля:

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

data

Полученные данные.

query.data

error

Объект ошибки.

query.error

isLoading

Первичная загрузка.

query.isLoading

isError

Флаг ошибки.

query.isError

isSuccess

Успешное получение данных.

query.isSuccess

isFetching

Любой активный запрос.

query.isFetching

refetch

Ручной перезапрос.

query.refetch()

Отличие isLoading и isFetching

Это один из важнейших моментов в библиотеке.

isLoading

Активен только при первой загрузке.

if (isLoading) {
    return <Spinner />
}

isFetching

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

  • первичная загрузка
  • фоновое обновление
  • ручной refetch
  • обновление при фокусе окна
{isFetching && <small>Обновление...</small>}

queryKey

queryKey — фундаментальная часть библиотеки.

Пример:

queryKey: ['users']

Ключ идентифицирует запрос в кэше.


Массивы в queryKey

Обычно используются массивы.

queryKey: ['user', userId]
queryKey: ['posts', category]
queryKey: ['products', filters]

Это позволяет:

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

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

function User({ userId }) {
    const query = useQuery({
        queryKey: ['user', userId],
        queryFn: () => fetchUser(userId)
    })
}

При изменении userId TanStack Query:

  1. создает новый кэш
  2. выполняет новый запрос
  3. сохраняет старые данные отдельно

queryFn

queryFn должна:

  • возвращать Promise
  • выбрасывать ошибки через throw
  • возвращать данные

Правильный пример:

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

    if (!response.ok) {
        throw new Error('Ошибка сервера')
    }

    return response.json()
}

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

Часто используется библиотека Axios.

import axios from 'axios'

const fetchUsers = async () => {
    const response = await axios.get('/api/users')

    return response.data
}

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

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

Разделение API-слоя

Хорошая практика — выносить API в отдельные файлы.

api/users.js

import axios from 'axios'

export const fetchUsers = async () => {
    const response = await axios.get('/api/users')

    return response.data
}

Компонент

import { useQuery } from '@tanstack/react-query'
import { fetchUsers } from './api/users'

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

Преимущества:

  • переиспользование
  • тестируемость
  • чистая архитектура
  • изоляция сетевой логики

Кэширование

После успешного запроса данные попадают в кэш.

Если другой компонент использует:

queryKey: ['users']

то повторный запрос не выполняется мгновенно.

Данные берутся из кэша.


staleTime

По умолчанию данные считаются устаревшими сразу после получения.

Настройка:

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

Здесь данные будут считаться актуальными одну минуту.


cacheTime

Определяет, сколько хранить неиспользуемый кэш.

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

В новых версиях используется gcTime.


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

TanStack Query автоматически обновляет данные:

  • при фокусе окна
  • при восстановлении соединения
  • при монтировании компонента

refetchOnWindowFocus

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

Отключает обновление при возврате на вкладку.


refetchOnReconnect

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

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


retry

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

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

retryDelay

Задержка между попытками.

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

Ручной refetch

function Users() {
    const {
        data,
        refetch
    } = useQuery({
        queryKey: ['users'],
        queryFn: fetchUsers
    })

    return (
        <div>
            <button onCl ick={() => refetch()}>
                Обновить
            </button>

            {data?.map(user => (
                <div key={user.id}>
                    {user.name}
                </div>
            ))}
        </div>
    )
}

Параллельные запросы

Можно использовать несколько useQuery.

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

const postsQuery = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts
})

Запросы выполняются параллельно.


Зависимые запросы

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

const postsQuery = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchPosts(userId),
    enabled: !!userQuery.data
})

Второй запрос начнется только после первого.


enabled

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

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

Обработка ошибок

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

if (isError) {
    return (
        <div>
            {error.message}
        </div>
    )
}

Placeholder Data

Временные данные до завершения запроса.

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

initialData

Начальные данные для кэша.

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

Разница placeholderData и initialData

placeholderData

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

initialData

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

Devtools

Подключение Devtools:

import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

<QueryClientProvider client={queryClient}>
    <App />

    <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>

Devtools позволяют:

  • просматривать кэш
  • видеть статусы запросов
  • анализировать обновления
  • отслеживать refetch
  • проверять stale/fresh состояния

useQuery и серверное состояние

Важно понимать различие между:

  • локальным состоянием
  • серверным состоянием

Локальное состояние

const [opened, setOpened] = useState(false)

Серверное состояние

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

Серверные данные:

  • приходят извне
  • могут устаревать
  • требуют синхронизации
  • могут изменяться другими пользователями

Именно для такого состояния создан TanStack Query.


Жизненный цикл запроса

Типичный жизненный цикл:

  1. Компонент монтируется
  2. Проверяется кэш
  3. Запускается запрос
  4. Данные кэшируются
  5. Компоненты получают данные
  6. Данные помечаются stale
  7. При необходимости выполняется refetch
  8. Неиспользуемый кэш удаляется через gcTime

Базовый шаблон useQuery

Наиболее распространенный шаблон:

const {
    data,
    isLoading,
    isError,
    error,
    isFetching
} = useQuery({
    queryKey: ['resource'],
    queryFn: fetchResource,
    staleTime: 1000 * 60
})

Рекомендуемая структура проекта

api

src/api/users.js
src/api/posts.js

hooks

src/hooks/useUsers.js
src/hooks/usePosts.js

components

src/components/Users.jsx

Кастомные хуки

import { useQuery } from '@tanstack/react-query'
import { fetchUsers } from '../api/users'

export const useUsers = () => {
    return useQuery({
        queryKey: ['users'],
        queryFn: fetchUsers
    })
}

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

function Users() {
    const { data } = useUsers()
}

Это позволяет:

  • скрывать детали реализации
  • переиспользовать запросы
  • централизовать настройки
  • упрощать компоненты

Основные преимущества TanStack Query

Автоматическое кэширование

Запросы переиспользуются между компонентами.

Дедупликация запросов

Несколько одинаковых запросов не создают лишний сетевой трафик.

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

Данные синхронизируются автоматически.

Управление ошибками

Встроенная система retry и error states.

Простая работа с асинхронностью

Исчезает необходимость в большом количестве useEffect.

Гибкая настройка

Можно управлять:

  • временем жизни кэша
  • refetch
  • retry
  • стратегией обновления
  • начальными данными
  • условиями запуска запросов