Использование selectFromResult

RTK Query предоставляет встроенный механизм подписки на данные через хуки, такие как useGetPostsQuery. Однако стандартная подписка возвращает весь объект результата запроса, включая data, isLoading, isFetching, error и другие поля. При росте приложения это приводит к избыточным перерендериваниям компонентов, даже если используется только небольшая часть данных.

selectFromResult решает эту проблему, позволяя явно указать, какие именно части результата запроса необходимы компоненту. Это создает локальный селектор поверх кеша RTK Query и существенно снижает количество лишних обновлений UI.


Базовый принцип работы selectFromResult

Каждый хук RTK Query подписывается на состояние запроса в Redux store. Без оптимизации компонент получает новый объект результата при любом изменении состояния запроса.

selectFromResult перехватывает этот процесс и позволяет:

  • выбрать только нужные поля
  • трансформировать результат перед передачей в компонент
  • изолировать перерендеры от изменений, которые не используются

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


Синтаксис использования

const { data } = useGetPostsQuery(undefined, {
  selectFromResult: (result) => ({
    data: result.data
  })
});

В данном случае компонент подписывается только на data. Изменения в isFetching, error или status не вызывают перерендер, если data остается неизменным.


Выбор нескольких полей результата

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

const { posts, loading } = useGetPostsQuery(undefined, {
  selectFromResult: (result) => ({
    posts: result.data,
    loading: result.isLoading
  })
});

Такой подход полезен при построении UI с раздельной логикой отображения состояния загрузки и данных.


Локальная фильтрация данных

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

const { activePosts } = useGetPostsQuery(undefined, {
  selectFromResult: (result) => ({
    activePosts: result.data?.filter(post => post.isActive)
  })
});

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


Мемоизация и поведение перерендеров

Важно понимать, что selectFromResult выполняется при каждом обновлении состояния запроса. Однако RTK Query использует shallow-equality для сравнения результата.

Перерендер произойдет только если:

  • изменился объект, возвращаемый селектором
  • изменились ссылки на используемые поля

Пример потенциальной проблемы:

selectFromResult: (result) => ({
  posts: result.data?.map(x => ({ ...x }))
})

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


Изоляция конкретного элемента данных

selectFromResult особенно полезен при работе с коллекциями, когда нужен доступ к одному элементу по ID.

const { post } = useGetPostsQuery(undefined, {
  selectFromResult: (result) => ({
    post: result.data?.find(p => p.id === 5)
  })
});

Такой подход позволяет компоненту подписаться только на конкретный объект, а не на весь массив.


Использование с параметрами запроса

selectFromResult работает не только с результатом, но и в контексте аргументов запроса.

const { post } = useGetPostQuery(postId, {
  selectFromResult: (result) => ({
    post: result.data
  })
});

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


Сравнение с мемоизированными селекторами Redux

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

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

В отличие от классических Redux-селекторов:

Подход Область применения Кеширование
createSelector глобальный store вручную
selectFromResult конкретный запрос встроено

Оптимизация сложных UI-компонентов

В сложных интерфейсах часто используется один запрос для нескольких визуальных блоков. Без selectFromResult каждый блок подписан на весь результат.

Пример разделения:

const { list } = useGetUsersQuery(undefined, {
  selectFromResult: (result) => ({
    list: result.data
  })
});

const { loading } = useGetUsersQuery(undefined, {
  selectFromResult: (result) => ({
    loading: result.isLoading
  })
});

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


Ошибки при использовании selectFromResult

1. Потеря стабильности ссылок

Нельзя создавать новые объекты без необходимости:

selectFromResult: (result) => ({
  data: [...result.data]
})

Каждый вызов создаёт новую ссылку, ломая мемоизацию.


2. Игнорирование undefined

При работе с асинхронными данными необходимо учитывать отсутствие data:

selectFromResult: (result) => ({
  count: result.data.length
})

Такой код приведет к ошибке при data === undefined.

Корректный вариант:

selectFromResult: (result) => ({
  count: result.data?.length ?? 0
})

3. Избыточные вычисления внутри селектора

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


Комбинирование с transformResponse

selectFromResult работает на уровне клиента, тогда как transformResponse — на уровне получения данных.

Разделение ответственности:

  • transformResponse — нормализация данных при загрузке
  • selectFromResult — выбор и адаптация данных под UI

Пример:

transformResponse: (response) => response.items
selectFromResult: (result) => ({
  active: result.data?.filter(x => x.active)
})

Работа с несколькими полями кеша

RTK Query хранит состояние запроса как единый объект. selectFromResult позволяет разбить его на логические части.

const { data, errorState } = useGetOrdersQuery(undefined, {
  selectFromResult: (result) => ({
    data: result.data,
    errorState: result.error
  })
});

Это особенно полезно для UI с разными состояниями отображения ошибок.


Поведенческая модель обновлений

При изменении кеша RTK Query:

  1. обновляется глобальное состояние запроса
  2. пересчитываются селекторы selectFromResult
  3. сравнивается результат
  4. компонент обновляется только при изменении ссылки

Это делает selectFromResult ключевым инструментом контроля ререндеров на уровне подписки.


Использование с пагинацией и фильтрацией

const { pageItems } = useGetItemsQuery(page, {
  selectFromResult: (result) => ({
    pageItems: result.data?.slice(0, 10)
  })
});

Такой подход позволяет не дублировать логику пагинации в каждом компоненте.


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

В больших приложениях RTK Query становится источником состояния для множества UI-компонентов. selectFromResult выполняет роль адаптера между:

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

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

  • уменьшить связанность компонентов с API
  • снизить количество перерендеров
  • локализовать вычислительную логику
  • повысить предсказуемость UI-обновлений

Типичные паттерны применения

Локальный селектор состояния загрузки

const { isLoading } = useGetProfileQuery(id, {
  selectFromResult: (result) => ({
    isLoading: result.isLoading
  })
});

Изоляция одного объекта

const { user } = useGetUsersQuery(undefined, {
  selectFromResult: (result) => ({
    user: result.data?.find(u => u.id === userId)
  })
});

Комбинирование данных и статуса

const { data, status } = useGetDataQuery(undefined, {
  selectFromResult: (result) => ({
    data: result.data,
    status: result.status
  })
});

Влияние на производительность

Использование selectFromResult особенно заметно в следующих сценариях:

  • большие списки данных (1000+ элементов)
  • частые обновления кеша (polling, refetchOnFocus)
  • сложные UI с множеством подписчиков на один endpoint

В этих условиях уменьшение числа перерендеров напрямую влияет на отзывчивость интерфейса.


Заключительная техническая особенность

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