Назначение и отличия от useQuery

Хук useQuery — центральный механизм библиотеки TanStack Query для получения, кэширования и синхронизации серверных данных в приложении. Его основная задача — избавить приложение от ручного управления состояниями загрузки, ошибок, кэширования и повторных запросов.

В классическом подходе компонент самостоятельно хранит:

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

Без специализированной библиотеки код быстро становится перегруженным.

Пример типичного подхода через useEffect:

import { useEffect, useState } from 'react'

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

  useEffect(() => {
    let cancelled = false

    async function fetchUsers() {
      try {
        setLoading(true)

        const response = await fetch('/api/users')

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

        const data = await response.json()

        if (!cancelled) {
          setUsers(data)
        }
      } catch (err) {
        if (!cancelled) {
          setError(err)
        }
      } finally {
        if (!cancelled) {
          setLoading(false)
        }
      }
    }

    fetchUsers()

    return () => {
      cancelled = true
    }
  }, [])

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

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

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

В простом примере код ещё выглядит приемлемо. Но при масштабировании появляются дополнительные задачи:

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

useQuery переносит всю инфраструктурную работу внутрь библиотеки.

Тот же пример через useQuery:

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>{error.message}</div>
  }

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

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


Что именно делает useQuery

useQuery отвечает за:

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

Фактически useQuery превращает серверное состояние в централизованный реактивный источник данных.


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

Одно из ключевых отличий TanStack Query от обычного useState заключается в разделении двух типов состояния.

Клиентское состояние

Клиентское состояние принадлежит интерфейсу:

const [isModalOpen, setIsModalOpen] = useState(false)

Примеры:

  • открыто ли меню;
  • выбран ли tab;
  • значение input;
  • состояние фильтра;
  • локальные переключатели.

Такое состояние полностью контролируется приложением.


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

Серверное состояние приходит извне:

const users = await fetch('/api/users')

Особенности серверного состояния:

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

Именно для серверного состояния создан useQuery.


Архитектурная проблема обычного fetch

Главная проблема стандартного подхода — отсутствие централизованного управления серверными данными.

Например:

function Sidebar() {
  // запрос пользователей
}

function Header() {
  // тот же запрос пользователей
}

function Profile() {
  // снова тот же запрос
}

Без общего кэша:

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

useQuery решает это через систему query cache.


Query Cache

Каждый запрос в useQuery хранится в централизованном кэше.

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

Ключ:

['users']

используется как идентификатор данных.

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

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

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

Будут использованы уже закэшированные данные.


Основные состояния useQuery

Хук предоставляет множество состояний.

Загрузка

const { isLoading } = useQuery(...)

Активно при первом запросе.


Ошибка

const { error } = useQuery(...)

Содержит объект ошибки.


Успешная загрузка

const { isSuccess } = useQuery(...)

Показывает успешное завершение.


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

const { isFetching } = useQuery(...)

Очень важное отличие от isLoading.

isLoading:

  • первый запрос;
  • данных ещё нет.

isFetching:

  • выполняется любой запрос;
  • данные уже могут существовать.

Пример:

if (isFetching) {
  console.log('Идёт обновление')
}

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


Отличие от useEffect

useEffect — механизм жизненного цикла

useEffect не предназначен специально для работы с серверными данными.

Он просто запускает побочный эффект:

useEffect(() => {
  fetchUsers()
}, [])

Все остальные задачи разработчик реализует вручную.


useQuery — специализированный data layer

useQuery — полноценный слой управления серверным состоянием.

Он автоматически:

  • кэширует данные;
  • отслеживает stale-состояние;
  • повторяет запросы;
  • предотвращает дублирование;
  • обновляет подписанные компоненты;
  • синхронизирует данные.

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

Одно из важнейших отличий.

При обычном fetch:

fetch('/api/users')
fetch('/api/users')
fetch('/api/users')

выполнятся три HTTP-запроса.

При использовании useQuery:

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

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

Остальные компоненты получают данные из cache.


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

Если несколько компонентов одновременно запрашивают одинаковые данные:

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

TanStack Query выполнит только один HTTP-запрос.

Все компоненты получат общий Promise.

Это называется request deduplication.


Повторные запросы

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

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

Это особенно важно при:

  • нестабильном интернете;
  • временных ошибках сервера;
  • мобильных сетях;
  • кратковременных таймаутах.

Background Refetch

Одно из фундаментальных отличий библиотеки — фоновая синхронизация данных.

Например:

  1. Пользователь открыл страницу.
  2. Данные загрузились.
  3. Пользователь переключился на другую вкладку.
  4. Вернулся обратно.

useQuery может автоматически обновить данные:

useQuery({
  queryKey: ['notifications'],
  queryFn: fetchNotifications,
  refetchOnWindowFocus: true
})

Это создаёт ощущение «живого» интерфейса.


Stale Data

В TanStack Query данные делятся на:

  • fresh;
  • stale.

Fresh:

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

Stale:

  • могут быть устаревшими;
  • библиотека может инициировать refetch.

Пример:

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

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


Разделение queryKey и queryFn

useQuery строится вокруг двух ключевых сущностей.

queryKey

Уникальный идентификатор данных.

['users']

или:

['user', userId]

queryFn

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

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

  return response.json()
}

Такое разделение позволяет:

  • централизованно кэшировать данные;
  • повторно использовать запросы;
  • гибко инвалидировать кэш.

Реактивность useQuery

Когда данные изменяются:

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

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

Это похоже на глобальное реактивное хранилище серверных данных.


Отличие от Redux

Многие приложения раньше хранили серверные данные в Redux.

Пример:

store.users.data
store.users.loading
store.users.error

Проблема такого подхода:

  • Redux не решает fetching;
  • Redux не управляет кэшем автоматически;
  • Redux не знает об устаревании данных;
  • разработчик вручную пишет reducers и actions.

useQuery специализируется именно на server state.


Отличие от SWR

SWR и TanStack Query решают похожие задачи:

  • кэширование;
  • revalidation;
  • background fetching.

Но у TanStack Query:

  • более развитая система cache management;
  • мощная работа с mutations;
  • гибкая invalidation;
  • развитые devtools;
  • сложные сценарии pagination и infinite queries;
  • расширенные механизмы синхронизации.

SWR обычно проще и легче, но менее функционален.


Отличие от Apollo Client

Apollo Client ориентирован на GraphQL.

useQuery из TanStack Query:

  • не привязан к GraphQL;
  • работает с REST;
  • работает с GraphQL;
  • работает с WebSocket;
  • работает с любыми Promise.

Пример:

useQuery({
  queryKey: ['products'],
  queryFn: () => axios.get('/api/products')
})

или:

useQuery({
  queryKey: ['graphql-users'],
  queryFn: fetchGraphQL
})

Отличие от обычного глобального store

Глобальные store вроде:

  • Redux;
  • Zustand;
  • MobX;

обычно используются для client state.

Но серверное состояние имеет другую природу:

  • асинхронность;
  • устаревание;
  • синхронизация;
  • кэширование;
  • фоновые обновления.

useQuery предоставляет специализированную инфраструктуру именно для таких задач.


Основная философия TanStack Query

Ключевая идея библиотеки:

«Сервер — источник истины».

Клиент не пытается постоянно копировать серверное состояние вручную.

Вместо этого:

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

Это фундаментальное отличие от старых подходов, где разработчик полностью контролировал загрузку и хранение данных самостоятельно.


Когда useQuery особенно полезен

REST API

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

Панели администратора

Большое количество таблиц и списков:

  • пользователи;
  • товары;
  • заказы;
  • уведомления.

Real-time интерфейсы

Где требуется периодическая синхронизация.


Большие SPA

С множеством повторно используемых данных.


Мобильные приложения

Где важны:

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

Когда useQuery может быть избыточным

Для простых приложений:

fetch('/api/ping')

использование полноценного query layer может быть неоправданным.

Особенно если:

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

Но по мере роста приложения преимущества становятся всё заметнее.


Главное отличие от ручного fetch

Ручной fetch:

fetch()
useEffect()
useState()

— это низкоуровневый механизм.

useQuery:

useQuery()

— полноценная система управления серверным состоянием.

Она включает:

  • query cache;
  • lifecycle запросов;
  • retry;
  • stale management;
  • background refetch;
  • deduplication;
  • synchronization;
  • garbage collection.

Именно поэтому TanStack Query считается не просто библиотекой для запросов, а полноценным серверным state manager для React-приложений.