Основные проблемы, которые решает TanStack Query

В клиентских приложениях существует два фундаментально разных типа данных:

  • локальное состояние интерфейса;
  • серверное состояние.

Локальное состояние полностью контролируется приложением: состояние модальных окон, активные вкладки, поля формы, переключатели интерфейса.

Серверное состояние принципиально отличается:

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

До появления TanStack Query разработчики обычно управляли серверными данными вручную через useEffect, fetch, axios, Redux, Context API или собственные сервисы. Это приводило к большому количеству повторяющегося кода и сложной логике синхронизации.

Пример типичного ручного запроса:

import { useEffect, useState } from 'react';

function UsersPage() {
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);

    useEffect(() => {
        let active = true;

        async function loadUsers() {
            try {
                setLoading(true);

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

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

                const data = await response.json();

                if (active) {
                    setUsers(data);
                }
            } catch (err) {
                setError(err);
            } finally {
                setLoading(false);
            }
        }

        loadUsers();

        return () => {
            active = false;
        };
    }, []);

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

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

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

Даже такой простой пример уже содержит:

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

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


Избыточный boilerplate-код

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

Практически каждый запрос требует:

  • loading;
  • error;
  • try/catch;
  • useEffect;
  • useState;
  • очистку эффектов;
  • повторную загрузку;
  • обработку статусов ответа.

Типичный пример:

const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
    async function fetchData() {
        try {
            setIsLoading(true);

            const response = await api.get();

            setData(response);
        } catch (e) {
            setError(e);
        } finally {
            setIsLoading(false);
        }
    }

    fetchData();
}, []);

В крупных проектах подобные конструкции могут повторяться сотни раз.

TanStack Query убирает этот шаблонный код:

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

Библиотека автоматически:

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

Отсутствие централизованного кэша

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

Например:

  1. пользователь открывает страницу списка товаров;
  2. товары загружаются;
  3. пользователь переходит на другую страницу;
  4. возвращается обратно;
  5. запрос выполняется снова.

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

  • лишняя нагрузка на сервер;
  • увеличение времени ожидания;
  • ненужный сетевой трафик;
  • ухудшение UX.

TanStack Query создаёт централизованный кэш серверных данных.

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

После первой загрузки данные помещаются в кэш. При повторном открытии страницы библиотека может:

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

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

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

Пример:

function Sidebar() {
    const users = useUsers();
}

function Dashboard() {
    const users = useUsers();
}

function Header() {
    const users = useUsers();
}

Если useUsers() внутри вызывает обычный fetch, сервер может получить три одинаковых запроса.

TanStack Query решает проблему дедупликации запросов.

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

Все компоненты используют общий query cache.

Результат:

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

Проблема устаревших данных

Серверные данные могут изменяться в любой момент:

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

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

TanStack Query предоставляет механизмы автоматической актуализации:

useQuery({
    queryKey: ['notifications'],
    queryFn: fetchNotifications,
    refetchInterval: 5000
});

Возможности:

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

Сложность фонового обновления

Ручная реализация background refetch обычно очень сложна.

Необходимо учитывать:

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

TanStack Query автоматизирует фоновые обновления:

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

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


Race conditions и проблемы конкурентности

Асинхронные запросы могут завершаться в непредсказуемом порядке.

Пример проблемы:

async function search(query) {
    const result = await fetch(`/search?q=${query}`);
    return result.json();
}

Если пользователь быстро вводит:

a
ab
abc

Запрос для a может завершиться позже запроса abc.

Результат:

  • интерфейс покажет старые данные;
  • состояние станет неконсистентным.

TanStack Query умеет:

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

Сложность обработки ошибок

В обычных приложениях обработка ошибок часто становится хаотичной:

try {
    const data = await fetchData();
} catch (e) {
    console.error(e);
}

Проблемы:

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

TanStack Query предоставляет встроенную систему обработки ошибок:

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

Возможности:

  • автоматические повторы;
  • контроль retry;
  • централизованная обработка;
  • error boundaries;
  • глобальные обработчики.

Проблема ручного обновления интерфейса после мутаций

После изменения данных необходимо синхронизировать интерфейс.

Например:

await createTodo(todo);

После создания задачи требуется:

  • обновить список;
  • обновить счётчики;
  • обновить статистику;
  • обновить связанные компоненты.

Без специальных инструментов разработчики часто вручную вызывают повторные запросы.

TanStack Query использует механизм invalidation:

const queryClient = useQueryClient();

const mutation = useMutation({
    mutationFn: createTodo,
    onSuccess: () => {
        queryClient.invalidateQueries({
            queryKey: ['todos']
        });
    }
});

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


Проблема optimistic updates

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

Например:

  1. пользователь нажимает “лайк”;
  2. интерфейс ждёт ответа сервера;
  3. только потом обновляется UI.

Это создаёт ощущение медленного интерфейса.

TanStack Query поддерживает optimistic updates:

useMutation({
    mutationFn: updatePost,
    onMutate: async (newPost) => {
        await queryClient.cancelQueries(['posts']);

        const previousPosts = queryClient.getQueryData(['posts']);

        queryClient.setQueryData(['posts'], old => {
            return old.map(post =>
                post.id === newPost.id
                    ? newPost
                    : post
            );
        });

        return { previousPosts };
    }
});

Интерфейс обновляется мгновенно ещё до ответа сервера.


Отсутствие автоматической синхронизации между компонентами

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

Проблемы:

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

TanStack Query использует единый query cache.

Все компоненты, использующие одинаковый queryKey, автоматически получают одинаковые данные.


Сложность управления жизненным циклом запросов

Обычные запросы требуют ручного контроля:

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

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

useQuery({
    queryKey: ['profile'],
    queryFn: fetchProfile,
    enabled: isAuthenticated
});

Библиотека умеет:

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

Проблема производительности

Избыточные запросы и повторные рендеры ухудшают производительность.

Типичные причины:

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

TanStack Query оптимизирует работу приложения:

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

Сложность pagination и infinite scrolling

Ручная реализация пагинации обычно сопровождается большим количеством логики:

const [page, setPage] = useState(1);
const [items, setItems] = useState([]);

Необходимо учитывать:

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

TanStack Query предоставляет специализированные механизмы:

useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    getNextPageParam: lastPage => lastPage.nextCursor
});

Библиотека автоматически управляет:

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

Проблема офлайн-режима

При потере сети обычные приложения часто работают некорректно:

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

TanStack Query умеет:

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

Проблема масштабирования приложения

По мере роста проекта ручное управление серверным состоянием становится всё сложнее.

Появляются:

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

TanStack Query создаёт единый стандартизированный подход к работе с серверными данными:

useQuery({...})
useMutation({...})
useInfiniteQuery({...})

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

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

Проблема смешивания серверного и клиентского состояния

Без специализированного инструмента разработчики часто помещают серверные данные в Redux или Context API.

Однако серверное состояние имеет совершенно другую природу:

Клиентское состояние Серверное состояние
полностью контролируется клиентом контролируется сервером
синхронное асинхронное
не требует кэширования требует кэширования
локальное удалённое
стабильное может устаревать

TanStack Query отделяет серверное состояние от UI-state.

Это делает архитектуру приложения значительно чище.


Проблема ручной инвалидизации кэша

При изменении данных необходимо понимать:

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

TanStack Query предоставляет централизованную систему invalidation:

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

Инвалидация может быть:

  • точечной;
  • групповой;
  • иерархической;
  • условной.

Проблема сложности SSR и hydration

При серверном рендеринге требуется:

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

TanStack Query поддерживает hydration/dehydration:

dehydrate(queryClient)
hydrate(queryClient, dehydratedState)

Это особенно важно для:

  • Next.js;
  • Remix;
  • SSR-приложений;
  • SEO-оптимизации.

Проблема отсутствия декларативного подхода

Традиционный код запросов часто строится императивно:

loadData();
reloadData();
fetchAgain();

TanStack Query использует декларативную модель:

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

Разработчик описывает:

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

Остальную работу библиотека берёт на себя.