Сравнение с другими решениями для управления состоянием

TanStack Query решает задачу управления серверным состоянием — данными, которые находятся вне приложения и синхронизируются через HTTP, WebSocket или другие транспортные механизмы.

К серверному состоянию относятся:

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

Главная особенность библиотеки заключается в том, что она не пытается заменить глобальное состояние приложения полностью. TanStack Query концентрируется именно на асинхронных данных и их жизненном цикле.

Это принципиально отличает её от:

  • Redux;
  • MobX;
  • Zustand;
  • Recoil;
  • Context API;
  • Vuex;
  • Pinia.

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

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

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

Хранится исключительно внутри приложения:

const [theme, setTheme] = useState('dark');

Примеры:

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

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

Приходит извне:

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

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

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

Именно с этим типом данных работает TanStack Query.


Сравнение с Redux

Подход Redux

Redux — централизованное хранилище состояния.

Типичная схема работы:

  1. dispatch action;
  2. reducer изменяет state;
  3. store обновляется;
  4. компоненты перерисовываются.

Загрузка данных в Redux

Классический Redux требует большого количества инфраструктурного кода.

Пример

const fetchUsers = () => async (dispatch) => {
    dispatch({ type: 'USERS_LOADING' });

    try {
        const response = await fetch('/api/users');
        const data = await response.json();

        dispatch({
            type: 'USERS_SUCCESS',
            payload: data
        });
    } catch (error) {
        dispatch({
            type: 'USERS_ERROR',
            payload: error
        });
    }
};

Дополнительно потребуются:

  • actions;
  • reducers;
  • selectors;
  • middleware;
  • thunk/saga;
  • ручной кэш;
  • контроль актуальности данных.

Аналогичная задача в TanStack Query

const usersQuery = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
        const response = await fetch('/api/users');
        return response.json();
    }
});

TanStack Query автоматически предоставляет:

  • кэширование;
  • дедупликацию запросов;
  • retries;
  • background refetch;
  • stale management;
  • loading state;
  • error state;
  • garbage collection;
  • window refetch;
  • reconnect refetch.

Объём кода

Redux

Для одного endpoint часто требуются:

  • action types;
  • action creators;
  • reducer;
  • thunk;
  • selectors;
  • store configuration.

TanStack Query

Обычно достаточно:

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

Разница особенно заметна в крупных приложениях с большим количеством API-запросов.


Кэширование

Redux

Redux не содержит встроенного кэша.

Разработчику приходится самостоятельно:

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

TanStack Query

Кэш встроен изначально.

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

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

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

Инвалидация данных

Redux

После mutation приходится вручную обновлять store.

dispatch(updatePost(post));
dispatch(fetchPosts());

TanStack Query

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

Инвалидация становится декларативной и централизованной.


Нормализация данных

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

{
    users: {
        byId: {},
        allIds: []
    }
}

TanStack Query обычно хранит данные в виде отдельных query-кэшей.

Это упрощает архитектуру и уменьшает количество преобразований.


Redux Toolkit Query и TanStack Query

Появление RTK Query стало ответом на проблемы классического Redux при работе с серверным состоянием.

RTK Query заимствует множество идей TanStack Query:

  • query hooks;
  • автоматический кэш;
  • invalidation;
  • polling;
  • optimistic updates.

Отличия RTK Query

RTK Query жёстко связан с Redux

Необходим store:

configureStore({
    reducer: {
        [api.reducerPath]: api.reducer
    }
});

TanStack Query независим

<QueryClientProvider client={queryClient}>

Нет необходимости строить полноценную Redux-инфраструктуру.


Когда Redux всё ещё полезен

Redux остаётся актуальным для:

  • сложной бизнес-логики;
  • глобального клиентского состояния;
  • event-driven архитектуры;
  • middleware-цепочек;
  • сложных state transitions.

TanStack Query не предназначен для замены всего state management.


Сравнение с Context API

Назначение Context API

Context API предназначен для передачи данных через дерево компонентов.

<ThemeContext.Provider value={theme}>

Это не полноценное решение для серверного состояния.


Ограничения Context API

Context не предоставляет:

  • кэширование;
  • refetch;
  • retries;
  • deduplication;
  • background updates;
  • mutation lifecycle.

Проблема лишних ререндеров

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

<UserContext.Provider value={users}>

Большие объёмы данных приводят к:

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

TanStack Query и подписки

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

Компонент обновляется только при изменении конкретной query.


Асинхронная логика

Context API не содержит встроенной модели async state.

Приходится вручную реализовывать:

const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

TanStack Query предоставляет это автоматически.


Сравнение с MobX

Подход MobX

MobX использует реактивную модель состояния.

class UserStore {
    users = [];

    constructor() {
        makeAutoObservable(this);
    }
}

Изменения автоматически отслеживаются.


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

MobX хорошо подходит для:

  • сложных взаимосвязанных состояний;
  • реактивных вычислений;
  • rich-client приложений;
  • desktop-like интерфейсов.

Работа с серверными данными

MobX не предоставляет готовую модель server state management.

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

  • кэш;
  • polling;
  • retries;
  • invalidation;
  • stale state;
  • background sync.

Смешивание client state и server state

В MobX часто возникает ситуация:

class Store {
    users = [];
    modalOpen = false;
    loading = false;
}

Клиентское и серверное состояние смешиваются в одном store.

TanStack Query разделяет эти концепции архитектурно.


Автоматическая синхронизация

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

  • при focus окна;
  • при reconnect;
  • при invalidation;
  • при повторном mount.

MobX требует ручной реализации подобных механизмов.


Сравнение с Zustand

Минимализм Zustand

Zustand — лёгкое state management решение.

const useStore = create((set) => ({
    bears: 0,
    increase: () => set((state) => ({
        bears: state.bears + 1
    }))
}));

Сильные стороны Zustand

Zustand удобен для:

  • UI state;
  • локального глобального состояния;
  • простых приложений;
  • минимального boilerplate.

Работа с API

Для серверных запросов Zustand обычно требует ручной реализации:

const useStore = create((set) => ({
    users: [],
    loading: false,

    fetchUsers: async () => {
        set({ loading: true });

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

        set({
            users,
            loading: false
        });
    }
}));

Проблемы ручного подхода

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

  • retries;
  • cache expiration;
  • deduplication;
  • optimistic updates;
  • race condition handling;
  • refetch policies.

В результате store усложняется.


Совместное использование Zustand и TanStack Query

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

Zustand

Используется для:

  • UI state;
  • filters;
  • theme;
  • modals;
  • client preferences.

TanStack Query

Используется для:

  • API data;
  • cache;
  • synchronization;
  • mutations;
  • background updates.

Сравнение с SWR

Общая концепция

SWR и TanStack Query очень близки идеологически.

Обе библиотеки:

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

Пример SWR

const { data, error, isLoading } = useSWR(
    '/api/users',
    fetcher
);

Простота SWR

SWR часто воспринимается как более минималистичное решение.

Особенно для:

  • небольших приложений;
  • простых REST API;
  • базового fetching.

Возможности TanStack Query

TanStack Query предоставляет существенно больше возможностей.

Infinite Queries

useInfiniteQuery()

Mutation Lifecycle

useMutation({
    onMutate,
    onError,
    onSuccess
});

Fine-Grained Cache Control

cacheTime
staleTime
gcTime

Devtools

Полноценные инструменты анализа query-кэша.


Query Invalidation

queryClient.invalidateQueries()

Dependent Queries

enabled: !!userId

Архитектурная сложность

SWR проще для изучения.

TanStack Query мощнее, но имеет:

  • больше концепций;
  • более сложный lifecycle;
  • расширенную конфигурацию.

Сравнение с Apollo Client

Специализация Apollo

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

Основные возможности:

  • normalized cache;
  • schema awareness;
  • GraphQL tooling;
  • subscriptions;
  • fragments.

TanStack Query не зависит от транспорта

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

  • REST;
  • GraphQL;
  • gRPC;
  • WebSocket;
  • custom async sources.

Нормализованный кэш Apollo

Apollo автоматически связывает сущности:

{
  user {
    id
    name
  }
}

Изменение entity обновляет связанные запросы.


Подход TanStack Query

TanStack Query использует query-based cache.

Кэш строится вокруг queryKey:

['users', userId]

Гибкость

TanStack Query менее навязчив архитектурно.

Нет необходимости:

  • описывать schema;
  • настраивать normalization;
  • поддерживать GraphQL conventions.

Когда Apollo лучше

Apollo особенно полезен при:

  • крупных GraphQL-системах;
  • federation;
  • сложных GraphQL schema;
  • fragment-heavy архитектуре.

Сравнение с Recoil

Концепция атомов

Recoil использует атомарное состояние.

const userState = atom({
    key: 'userState',
    default: null
});

Асинхронные селекторы

const usersQuery = selector({
    key: 'usersQuery',
    get: async () => {
        const response = await fetch('/api/users');
        return response.json();
    }
});

Отличия от TanStack Query

Recoil ориентирован на:

  • state graph;
  • derived state;
  • dependency graph;
  • fine-grained reactivity.

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


Cache Invalidation

В Recoil обычно требуется ручное управление зависимостями.

TanStack Query предоставляет готовую модель:

invalidateQueries()

Сравнение с Vuex и Pinia

Vuex

Vuex исторически использовался как централизованный store для Vue-приложений.

Недостатки при работе с серверным состоянием:

  • большое количество boilerplate;
  • ручное кэширование;
  • ручной async lifecycle;
  • сложность invalidation.

Pinia

Pinia существенно упростила архитектуру.

export const useStore = defineStore('users', {
    state: () => ({
        users: []
    })
});

Однако проблема server state остаётся

Даже с Pinia приходится самостоятельно реализовывать:

  • refetch;
  • stale policies;
  • retries;
  • deduplication;
  • synchronization.

Vue Query

Для Vue существует адаптация TanStack Query:

useQuery()

Подход остаётся тем же:

  • серверное состояние отдельно;
  • клиентское состояние отдельно.

Архитектурные преимущества TanStack Query

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

Вместо императивного управления:

fetchData();
setLoading(true);

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

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

Автоматический lifecycle

TanStack Query самостоятельно управляет:

  • loading;
  • fetching;
  • stale;
  • inactive;
  • garbage collection.

Server State First Architecture

Библиотека строится вокруг идеи:

серверное состояние — отдельный тип данных.

Это одна из главных причин популярности TanStack Query.


Когда TanStack Query не нужен

Библиотека может быть избыточной для:

  • полностью локальных приложений;
  • небольших SPA без сложного API;
  • простых CRUD-интерфейсов;
  • приложений без кэширования.

Комбинирование решений

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

Популярная комбинация

TanStack Query

Для:

  • API;
  • cache;
  • async state.

Zustand или Redux

Для:

  • UI state;
  • auth state;
  • wizard state;
  • complex client workflows.

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

TanStack Query

Отвечает за:

  • серверные данные;
  • запросы;
  • кэш;
  • синхронизацию;
  • mutations.

State Manager

Отвечает за:

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

Ключевое отличие TanStack Query

Большинство state manager библиотек пытаются универсально хранить любые данные.

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

  • query cache;
  • stale management;
  • invalidation;
  • background synchronization;
  • request deduplication;
  • optimistic updates;
  • retry system;
  • automatic refetching.

Именно эта специализация сделала библиотеку одним из стандартов современного frontend-разработки.