Мемоизация селекторов

В архитектуре RTK Query ключевую роль играет минимизация лишних перерисовок и повторных вычислений производных данных. Одним из центральных механизмов оптимизации выступают мемоизированные селекторы, позволяющие эффективно извлекать данные из кэша Redux без создания новых ссылок при каждом обращении к состоянию.

RTK Query строится поверх Redux Toolkit и использует стандартную концепцию селекторов, расширяя её возможностями кэширования, сравнения аргументов и повторного использования вычисленных результатов. Понимание того, как работает мемоизация в этом контексте, критично для масштабируемых приложений с большим количеством запросов и подписок на данные.


Роль селекторов в RTK Query

Селекторы в RTK Query используются для доступа к данным, хранящимся в query cache. Каждый endpoint генерирует набор селекторов, которые позволяют:

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

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


Проблема повторных вычислений

Без мемоизации каждый вызов селектора приводил бы к созданию новых объектов:

const data = useSelector(state => state.api.queries.getUser({ id: 1 }));

Даже если данные в кэше не изменились, новый объект возвращался бы при каждом обращении, что приводило бы к:

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

RTK Query решает эту проблему через встроенную мемоизацию на уровне селекторов.


Механизм memoization в RTK Query

RTK Query использует комбинацию:

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

Каждый endpoint создаёт селекторную фабрику, которая возвращает новый селектор только при изменении входных аргументов.

Принцип работы

  1. Создаётся селектор-фабрика для endpoint.
  2. При первом вызове с конкретными аргументами создаётся селектор.
  3. Результат кэшируется по ключу аргументов.
  4. Повторный вызов с теми же аргументами возвращает тот же селектор.

createSelector и внутренняя мемоизация

RTK Query опирается на концепцию мемоизации, аналогичную createSelector из Reselect.

Пример базового селектора:

const selectUser = api.endpoints.getUser.select({ id: 1 });

Этот вызов возвращает мемоизированную функцию, которая:

  • читает состояние Redux;
  • извлекает соответствующий cache entry;
  • возвращает строго идентичный объект при неизменности данных.

Селекторная фабрика endpoint’а

Каждый endpoint в RTK Query имеет метод select, возвращающий фабрику селекторов:

const selectUserFactory = api.endpoints.getUser.select;

const selectUser1 = selectUserFactory({ id: 1 });
const selectUser2 = selectUserFactory({ id: 2 });

Важно, что:

  • selectUserFactory не содержит конкретных данных;
  • каждый вызов создаёт мемоизированный селектор под конкретные аргументы;
  • одинаковые аргументы возвращают один и тот же селектор (при повторном обращении внутри одного жизненного цикла).

Кэширование селекторов по аргументам

RTK Query использует внутреннюю структуру Map-подобного кэша:

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

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

  • избегать повторного создания функций;
  • обеспечивать стабильность ссылок;
  • снижать нагрузку на React reconciliation.

Пример логики:

selectorsCache = {
  "getUser({id:1})": selectorFn,
  "getUser({id:2})": selectorFn
}

Стабильность ссылок и shallow equality

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

Если данные в кэше не изменились, RTK Query возвращает тот же объект:

const user = useSelector(api.endpoints.getUser.select({ id: 1 }));

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

  • объект user остаётся тем же;
  • React не вызывает повторный рендер зависимых компонентов;
  • useSelector проходит shallow comparison без изменений.

Взаимодействие с Redux state

Селекторы RTK Query не работают напрямую с бизнес-логикой приложения. Они читают только:

  • state.api.queries
  • state.api.mutations
  • state.api.subscriptions

Структура запроса внутри state:

state.api.queries["getUser({id:1})"]

Селектор извлекает запись и возвращает нормализованный объект:

{
  data,
  status,
  error,
  requestId,
  fulfilledTimeStamp
}

Мемоизация внутри useSelector

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

const userQuery = useSelector(api.endpoints.getUser.select({ id: 1 }));

происходит двойная мемоизация:

  1. мемоизация селектора RTK Query;
  2. мемоизация результата useSelector.

Это означает:

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

Отличие от ручных селекторов

Обычные селекторы Redux:

const selectUser = state => state.users.byId[1];

Мемоизация отсутствует, если не использовать createSelector.

RTK Query селекторы:

  • мемоизированы автоматически;
  • зависят от аргументов запроса;
  • привязаны к кэшу API слоя.

Инвалидация и влияние на мемоизацию

Мемоизация селекторов тесно связана с системой инвалидации кэша.

При:

  • invalidateTags
  • refetch
  • mutation success

происходит:

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

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


Селекторы и подписки

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

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

Это снижает:

  • количество listeners в store;
  • нагрузку на middleware;
  • частоту обновлений UI.

Производительность при большом количестве endpoints

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

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

Особенно это заметно при:

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

Ошибки при неправильном использовании селекторов

Распространённые проблемы:

Создание селектора внутри компонента без стабилизации

const user = useSelector(api.endpoints.getUser.select({ id: props.id }));

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

Нестабильные аргументы

select({ id: Math.random() })

Каждый вызов создаёт новый ключ кэша.


Оптимизационная модель RTK Query

Мемоизация селекторов является частью общей стратегии:

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

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


Связь мемоизации селекторов с архитектурой Redux Toolkit

RTK Query не вводит отдельную концепцию состояния — он расширяет Redux store через предсказуемые правила:

  • state остаётся неизменяемым;
  • селекторы возвращают производные данные;
  • мемоизация снижает стоимость вычислений;
  • кеш становится источником истины для UI-слоя.

Так формируется модель, в которой доступ к данным становится не вычислительно дорогой операцией, а предсказуемым чтением из оптимизированного кэша.