Автоматическая подписка и отписка

В основе работы RTK Query лежит система подписок (subscriptions), которая связывает жизненный цикл запроса с компонентами приложения. Каждый раз, когда используется useQuery или любой другой query-хук, создаётся подписка на конкретный запрос в кэше. Эта подписка управляет тем, когда данные должны быть запрошены, обновлены или удалены.

Подписка в RTK Query — это не просто факт «компонент использует данные», а полноценный объект состояния, включающий:

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

Создание подписки при первом использовании запроса

При первом вызове:

const { data } = useGetUsersQuery();

RTK Query выполняет несколько шагов:

  1. Проверяет наличие кэшированного результата по ключу endpoint + args.
  2. Если данных нет или они устарели, инициирует запрос.
  3. Создаёт запись в кэше (cache entry).
  4. Увеличивает счётчик подписок для данного запроса.

Ключевой момент: каждая уникальная комбинация аргументов создаёт отдельную подписку. Например:

useGetUserQuery(1);
useGetUserQuery(2);

создают две независимые подписки.


Увеличение и уменьшение счётчика подписок

Каждый компонент, использующий один и тот же запрос с одинаковыми аргументами, увеличивает счётчик подписок:

  • монтирование компонента → subscriptionCount + 1
  • размонтирование компонента → subscriptionCount - 1

Когда счётчик достигает нуля, RTK Query не удаляет данные сразу, а запускает таймер удержания кэша.


Удержание данных после отписки

Параметр keepUnusedDataFor определяет, сколько времени данные остаются в кэше после того, как последняя подписка была удалена.

createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  keepUnusedDataFor: 60,
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => '/users'
    })
  })
});

Поведение:

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

Автоматическое обновление при повторной подписке

Если компонент повторно монтируется до истечения keepUnusedDataFor, RTK Query:

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

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


Поведение при изменении аргументов запроса

Каждое изменение аргументов воспринимается как новая подписка:

useGetUserQuery(userId);

Если userId изменился:

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

Важно, что RTK Query не «перепривязывает» старую подписку, а полностью создаёт новую сущность.


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

Когда компонент перестаёт использовать данные:

function User() {
  const { data } = useGetUserQuery(1);
  return null;
}

при размонтировании:

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

Это поведение встроено в middleware RTK Query и не требует ручного управления.


Роль middleware в управлении подписками

Вся система подписок реализована через middleware RTK Query, которое:

  • отслеживает действия query initiated;
  • обновляет store состояния API slice;
  • управляет lifecycle cache entry;
  • координирует refetch и cleanup.

Store хранит структуру примерно следующего вида:

state.api.queries[endpointName][serializedArgs]

Каждая запись содержит:

  • status (pending / fulfilled / rejected);
  • data;
  • fulfilledTimeStamp;
  • subscriptions (map активных подписчиков).

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

Если компонент возвращается на экран:

useGetUsersQuery();

RTK Query:

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

refetchOnMountOrArgChange и подписки

Опция refetchOnMountOrArgChange влияет на поведение подписок при повторном подключении:

useGetUsersQuery(undefined, {
  refetchOnMountOrArgChange: true
});

Возможные режимы:

  • false — использовать кэш без обновления;
  • true — всегда выполнять запрос при монтировании;
  • number — считать данные устаревшими после времени.

Эта настройка работает поверх системы подписок и не заменяет её.


refetchOnFocus и refetchOnReconnect

RTK Query интегрируется с событиями браузера:

  • refetchOnFocus — обновление при возвращении вкладки;
  • refetchOnReconnect — обновление при восстановлении сети.

Система подписок участвует косвенно:

  • если есть активные подписчики — выполняется refetch;
  • если подписчиков нет — запрос не инициируется.

Политика активности подписок

Подписка считается активной, пока:

  • компонент смонтирован;
  • хук useQuery используется с теми же аргументами;
  • нет явного skip состояния.

Пример отключения подписки:

useGetUserQuery(id, { skip: !id });

При skip: true:

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

skipToken как альтернативная форма отписки

skipToken используется для условного отключения запроса:

import { skipToken } from '@reduxjs/toolkit/query';

useGetUserQuery(userId ?? skipToken);

Поведение:

  • если передан skipToken, подписка не создаётся;
  • отсутствует запись в subscriptions;
  • компонент не участвует в lifecycle запроса.

Влияние строгой идентичности аргументов

RTK Query сериализует аргументы запроса. Подписка зависит не от ссылки объекта, а от результата сериализации:

useGetUserQuery({ id: 1 });
useGetUserQuery({ id: 1 });

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

Это критически важно для:

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

Очистка кэша после завершения подписок

Когда:

  • все подписки удалены;
  • истёк keepUnusedDataFor;

RTK Query:

  • удаляет cache entry;
  • освобождает память;
  • сбрасывает метаданные запроса.

Если позже снова запрашиваются те же данные:

  • создаётся новая подписка;
  • выполняется новый запрос.

Поведение при конкурентных подписках

Если несколько компонентов используют один endpoint:

<ComponentA /> → useGetUsersQuery()
<ComponentB /> → useGetUsersQuery()

RTK Query:

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

При удалении одного компонента:

  • уменьшается счётчик;
  • запрос не повторяется, пока есть другие подписчики.

Перезапуск запроса при смене подписчиков

Если последний подписчик удалён и затем появляется новый:

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

Связь подписок с polling

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

useGetUsersQuery(undefined, {
  pollingInterval: 5000
});

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

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

Итоговая модель поведения подписок

Логика RTK Query строится вокруг трёх базовых принципов:

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

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