Базовый пример использования

Базовый пример работы с TanStack Query обычно строится вокруг загрузки данных с сервера и отображения состояний запроса: загрузки, ошибки и успешного ответа.

Установка библиотеки для React:

npm install @tanstack/react-query

Либо через Yarn:

yarn add @tanstack/react-query

После установки создаётся экземпляр QueryClient, который отвечает за хранение кэша, управление запросами, повторные попытки, обновления и синхронизацию состояния.


Создание QueryClient

На верхнем уровне приложения подключается QueryClientProvider.

import React fr om 'react';
import ReactDOM from 'react-dom/client';

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

import App from './App';

const queryClient = new QueryClient();

const root = ReactDOM.createRoot(
    document.getElementById('root')
);

root.render(
    <QueryClientProvider client={queryClient}>
        <App />
    </QueryClientProvider>
);

Что происходит в этом коде

QueryClient — центральный объект TanStack Query.

Он управляет:

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

QueryClientProvider передаёт экземпляр клиента через React Context всем компонентам приложения.

Без него хуки TanStack Query работать не будут.


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

Теперь можно создать простой компонент с загрузкой данных.

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

async function fetchUsers() {
    const response = await fetch(
        'https://jsonplaceholder.typicode.com/users'
    );

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

    return response.json();
}

export default function UsersList() {
    const {
        data,
        isLoading,
        isError,
        error,
    } = useQuery({
        queryKey: ['users'],
        queryFn: fetchUsers,
    });

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

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

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

Разбор useQuery

Хук useQuery — основной инструмент для получения серверных данных.

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

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

Параметр queryKey

queryKey: ['users']

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

Именно по нему TanStack Query хранит данные в кэше.

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

['users']

Либо более сложным:

['users', userId]

Или:

['posts', {
    page: 1,
    lim it: 10,
}]

Назначение queryKey

Ключ используется для:

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

Если два компонента используют одинаковый queryKey, запрос выполняется один раз.


Параметр queryFn

queryFn: fetchUsers

queryFn — функция загрузки данных.

Она должна:

  • возвращать Promise;
  • либо выбрасывать ошибку.

Пример:

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

    if (!response.ok) {
        throw new Error('Ошибка');
    }

    return response.json();
}

TanStack Query самостоятельно вызывает эту функцию и управляет её жизненным циклом.


Состояния запроса

Загрузка

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

Во время первого выполнения запроса isLoading будет равен true.


Ошибка

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

Если Promise завершился ошибкой, TanStack Query установит:

isError === true

А объект ошибки попадёт в error.


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

data.map(user => ...)

После успешного запроса данные становятся доступны через data.


Как работает кэширование

После первого запроса TanStack Query сохраняет данные в памяти.

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

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

библиотека:

  1. проверяет наличие данных в кэше;
  2. мгновенно отдаёт их компоненту;
  3. при необходимости запускает фоновое обновление.

Это устраняет:

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

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

Предположим, существуют два компонента:

<UsersSidebar />
<UsersTable />

Оба используют:

queryKey: ['users']

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

Все компоненты получат единый источник данных.

Это одно из ключевых преимуществ TanStack Query перед ручным использованием useEffect.


Что происходит без TanStack Query

Классический React-код часто выглядит так:

useEffect(() => {
    setLoading(true);

    fetch('/api/users')
        .then(response => response.json())
        .then(data => {
            setUsers(data);
        })
        .catch(error => {
            setError(error);
        })
        .finally(() => {
            setLoading(false);
        });
}, []);

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

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

TanStack Query автоматизирует эти задачи.


Автоматическое обновление данных

После сворачивания вкладки и возврата обратно TanStack Query может автоматически обновить данные.

Это поведение включено по умолчанию.

Пример:

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

Если пользователь вернётся на вкладку браузера спустя некоторое время, библиотека выполнит повторный запрос в фоне.


staleTime

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

Это можно изменить:

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

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

В течение этого времени TanStack Query не будет повторно обращаться к серверу.


gcTime

TanStack Query хранит данные в памяти даже после размонтирования компонентов.

Время хранения регулируется через gcTime.

const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            gcTime: 1000 * 60 * 10,
        },
    },
});

В данном случае кэш хранится 10 минут.


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

При ошибках TanStack Query автоматически повторяет запрос.

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

Библиотека выполнит до трёх повторных попыток.

Отключение повторов:

retry: false

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

TanStack Query умеет обновлять данные без полной перезагрузки интерфейса.

Во время фонового обновления:

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

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

isFetching

Пример:

const {
    data,
    isFetching,
} = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
});
{isFetching && <p>Обновление...</p>}

Полный базовый пример

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

const queryClient = new QueryClient();

async function fetchPosts() {
    const response = await fetch(
        'https://jsonplaceholder.typicode.com/posts'
    );

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

    return response.json();
}

function Posts() {
    const {
        data,
        isLoading,
        isError,
        error,
        isFetching,
    } = useQuery({
        queryKey: ['posts'],
        queryFn: fetchPosts,
        staleTime: 1000 * 30,
    });

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

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

    return (
        <div>
            {isFetching && (
                <p>Обновление данных...</p>
            )}

            {data.map(post => (
                <article key={post.id}>
                    <h3>{post.title}</h3>

                    <p>{post.body}</p>
                </article>
            ))}
        </div>
    );
}

export default function App() {
    return (
        <QueryClientProvider client={queryClient}>
            <Posts />
        </QueryClientProvider>
    );
}

Основные преимущества базового подхода

Минимизация boilerplate-кода

Без TanStack Query приходится вручную создавать:

  • loading;
  • error;
  • data;
  • useEffect;
  • повторные запросы;
  • отмену запросов;
  • кэширование.

С библиотекой большая часть логики уже реализована.


Единый источник серверного состояния

TanStack Query создаёт централизованное хранилище серверных данных.

Это устраняет:

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

Производительность

Благодаря кэшированию уменьшается количество HTTP-запросов.

Особенно заметно это в:

  • административных панелях;
  • CRM;
  • интернет-магазинах;
  • сложных SPA;
  • мобильных интерфейсах.

Упрощение архитектуры

Во многих случаях исчезает необходимость хранить серверные данные в:

  • Redux;
  • MobX;
  • Zustand;
  • Context API.

TanStack Query берёт на себя именно серверное состояние, оставляя глобальным хранилищам только клиентские данные интерфейса.


Типичный жизненный цикл запроса

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

useQuery(...)

происходит:

  1. поиск данных в кэше;
  2. запуск queryFn, если данных нет;
  3. установка isLoading;
  4. получение ответа;
  5. сохранение результата;
  6. уведомление всех подписанных компонентов;
  7. переход в успешное состояние.

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

  1. данные берутся из кэша;
  2. интерфейс отображается мгновенно;
  3. при необходимости запускается фоновое обновление.

Структура возвращаемого объекта useQuery

Хук возвращает большое количество свойств.

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

const {
    data,
    error,
    isLoading,
    isError,
    isSuccess,
    isFetching,
    refetch,
    status,
} = useQuery(...);

data

Содержит результат запроса.


error

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


isLoading

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


isFetching

Любой активный запрос, включая фоновые обновления.


isSuccess

Успешное выполнение запроса.


refetch

Ручной повторный запрос.

Пример:

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

Ручное обновление данных

const {
    data,
    refetch,
} = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
});
<button onCl ick={refetch}>
    Перезагрузить
</button>

TanStack Query выполнит новый запрос и обновит кэш.


Devtools

Для отладки существует отдельный пакет:

npm install @tanstack/react-query-devtools

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

import { ReactQueryDevtools }
from '@tanstack/react-query-devtools';
<QueryClientProvider client={queryClient}>
    <App />

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

Devtools позволяют:

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

Базовая схема работы TanStack Query

Компонент
    ↓
useQuery
    ↓
queryKey
    ↓
Проверка кэша
    ↓
queryFn
    ↓
HTTP-запрос
    ↓
Кэширование
    ↓
Обновление интерфейса

Наиболее частые ошибки начинающих

Отсутствие QueryClientProvider

Ошибка:

No QueryClient set

Причина:

useQuery(...)

используется вне QueryClientProvider.


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

Плохо:

queryKey: [Math.random()]

Кэширование перестанет работать корректно.


Выполнение побочных эффектов внутри queryFn

Плохо:

async function fetchUsers() {
    setState(...)

    return fetch(...)
}

queryFn должна заниматься только загрузкой данных.


Использование useQuery для мутаций

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

Для:

  • POST;
  • PUT;
  • PATCH;
  • DELETE;

используется useMutation.


Минимальный production-подход

Часто запросы выносятся в отдельные API-модули.

api/users.js

export async function fetchUsers() {
    const response = await fetch('/api/users');

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

    return response.json();
}

hooks/useUsers.js

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

import { fetchUsers } from '../api/users';

export function useUsers() {
    return useQuery({
        queryKey: ['users'],
        queryFn: fetchUsers,
    });
}

Компонент

import { useUsers } from './hooks/useUsers';

export default function UsersPage() {
    const { data } = useUsers();

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

Такой подход делает архитектуру:

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