RTK Query строит модель работы с данными вокруг идеи кэшированных запросов и подписок на конкретные части состояния, а не на весь результат запроса целиком. Это позволяет управлять тем, какие компоненты реагируют на изменения данных, минимизировать лишние ререндеры и изолировать влияние обновлений в кэше.
Каждый вызов хука запроса, например useGetUsersQuery(),
создаёт подписку на конкретный cache entry. RTK Query хранит данные по
ключу, сформированному из:
Внутри store это выглядит как запись вида:
state.api.queries[endpointName(argument)]
Несколько компонентов могут подписаться на один и тот же cache entry. В этом случае RTK Query увеличивает счётчик активных подписок, не дублируя запрос.
Ключевая особенность: по умолчанию компонент получает весь результат запроса и ререндерится при любом изменении данных этого cache entry, даже если фактически используется только его часть.
При работе с массивами или объектами, которые часто обновляются (например, списки пользователей, сообщения, товары), типичная проблема заключается в том, что любое изменение данных вызывает обновление всего результата:
const { data } = useGetUsersQuery();
Если изменился один элемент массива или пришли новые поля, ссылка на
data обновляется, и компонент перерендеривается
полностью.
При масштабировании приложения это приводит к:
Основной инструмент RTK Query для выборочной подписки —
selectFromResult. Он позволяет подписаться не на весь
результат запроса, а на его проекцию.
Вместо получения полного объекта результата выполняется селекция:
const { user } = useGetUsersQuery(undefined, {
selectFromResult: ({ data }) => ({
user: data?.find(u => u.id === 5)
})
});
В этом случае компонент зависит не от всего data, а
только от вычисленного user.
RTK Query отслеживает зависимости селектора и перерендеривает
компонент только при изменении результата функции
selectFromResult.
Когда приходит новый ответ или происходит инвалидация:
Это поведение делает selectFromResult аналогом
оптимизированного useSelector, но привязанного к
конкретному endpoint.
Важный аспект работы выборочной подписки — стабильность возвращаемых значений.
selectFromResult: ({ data }) => ({
user: data?.find(u => u.id === 5)
})
Если find возвращает новый объект при каждом вычислении
(а он возвращает ссылку на элемент массива), RTK Query всё равно будет
сравнивать результат селектора. Если ссылка не изменилась — ререндер не
произойдёт.
Однако если внутри создаются новые структуры:
selectFromResult: ({ data }) => ({
user: {
...data?.find(u => u.id === 5),
computed: true
}
})
то новый объект будет создаваться каждый раз, что приведёт к
постоянным ререндерам. В таких случаях требуется дополнительная
мемоизация через useMemo или вынос вычислений в
селекторы.
Частый кейс — отображение списков, где каждый элемент подписан только на свою сущность.
const selectUserById = (id) => (result) => ({
user: result.data?.entities?.[id]
});
const UserRow = ({ id }) => {
const { user } = useGetUsersQuery(undefined, {
selectFromResult: selectUserById(id)
});
};
Здесь каждый UserRow подписан только на конкретного
пользователя. При изменении другого элемента списка ререндер не
произойдёт.
Выборочная подписка особенно эффективна при использовании
нормализованных данных через transformResponse или
createEntityAdapter.
Пример структуры:
state.api.queries.getUsers.data = {
entities: {
1: {...},
2: {...}
},
ids: [1, 2]
}
В этом случае selectFromResult может точечно
подписываться на отдельные сущности без обхода массива.
selectFromResult: ({ data }) => ({
user: data?.entities[id]
})
Изменение одного entity не затрагивает остальные компоненты, подписанные на другие id.
selectFromResult концептуально похож на
useSelector, но имеет важное отличие:
useSelector работает с глобальным stateselectFromResult работает с конкретным cache entry RTK
QueryЭто означает, что RTK Query уже ограничивает область наблюдения, а
selectFromResult дополнительно сужает её до нужного
фрагмента данных.
При обновлении данных через:
pollingInterval)пересчёт selectFromResult происходит автоматически.
Если результат селектора не изменился, компонент остаётся стабильным, даже если сам запрос был выполнен повторно.
Это особенно важно при частых polling-интервалах, где полный ререндер списка был бы слишком дорогим.
Поскольку функция выполняется при каждом обновлении cache entry, важно избегать тяжёлых операций:
Плохой вариант:
selectFromResult: ({ data }) => ({
filtered: data?.filter(x => complexCheck(x))
})
Такой код выполняется при каждом обновлении данных и может стать узким местом.
Практика оптимизации:
selectFromResultRTK Query позволяет одному компоненту иметь несколько независимых подписок через разные вызовы хука:
const user = useGetUserQuery(id);
const posts = useGetUserPostsQuery(id);
Но более эффективный вариант — объединение через
selectFromResult, чтобы контролировать перерендеры на
уровне одного cache entry или минимального набора зависимостей.
При использовании useLazyQuery механизм подписки
остаётся тем же, но активация происходит вручную.
selectFromResult при этом позволяет ограничить реактивную
часть данных даже после триггера запроса:
const [trigger, result] = useLazyGetUsersQuery();
const { user } = useGetUsersQuery(undefined, {
selectFromResult: ({ data }) => ({
user: data?.[0]
})
});
Таким образом можно отделить момент получения данных от момента подписки на их часть.
RTK Query использует сравнение ссылок для определения необходимости
обновления. При работе с selectFromResult это становится
критически важным:
Поэтому любые новые ссылки, создаваемые в селекторе, автоматически приводят к обновлению компонента.
Выборочная подписка применяется не только для оптимизации, но и для архитектурного разделения:
Типичный паттерн:
Так достигается предсказуемое поведение обновлений без каскадных ререндеров.
Несмотря на гибкость, selectFromResult имеет
ограничения:
Поэтому выборочная подписка эффективна только при дисциплине управления данными и понимании механики кеша RTK Query.