Select для минимизации ререндеров

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

В React-архитектуре каждое изменение ссылки на объект данных приводит к потенциальному ререндеру компонентов. В контексте серверного состояния это становится особенно заметно: даже если фактические данные не изменились, но изменился их контейнер (новая ссылка на объект, новый массив, пересобранный ответ), React будет считать это обновлением.

В TanStack Query это поведение проявляется при:

  • повторной выборке данных (refetch)
  • инвалидации query
  • фоновых обновлениях (background refetch)
  • гидратации кэша
  • изменении структурного представления данных

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


Как работает select в TanStack Query

select — это функция трансформации данных, которая позволяет преобразовать результат запроса до того, как он попадёт в компонент.

const { data } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  select: (data) => data.map(user => user.name)
})

Ключевая особенность:

  • select выполняется после получения данных из кэша
  • результат select становится новым значением data в компоненте
  • позволяет уменьшить объём наблюдаемых данных

Таким образом, компонент подписывается не на весь объект ответа, а на его производную форму.


Почему select уменьшает количество ререндеров

Основной механизм оптимизации основан на сравнении ссылок.

TanStack Query использует структурное разделение и мемоизацию, но:

  • без select компонент получает полный объект данных
  • любые изменения внутри объекта могут менять ссылку
  • React воспринимает это как изменение props

С select:

  • можно сузить поверхность данных
  • уменьшить вероятность изменения ссылки
  • изолировать только нужные поля

Пример:

const { data } = useQuery({
  queryKey: ['user', id],
  queryFn: fetchUser,
  select: (user) => user.email
})

Если изменилось поле user.name, но email остался тем же, компонент не обязан ререндериться.


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

select не просто функция трансформации, она также участвует в механизме мемоизации результата запроса.

TanStack Query повторно использует предыдущие вычисления при следующих условиях:

  • те же входные данные из кэша
  • неизменность ссылки на исходные данные
  • стабильность функции select

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


Разделение ответственности: server state vs view state

select фактически создаёт слой проекции данных:

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

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

const { data } = useQuery({
  queryKey: ['products'],
  queryFn: fetchProducts,
  select: (products) => ({
    list: products.map(p => ({
      id: p.id,
      title: p.title
    })),
    count: products.length
  })
})

Важно: даже если исходный массив products изменится, компонент может не перерендериться, если результат select не изменил ссылку.


select и стабильность ссылок

Ключевой момент оптимизации — управление referential equality.

TanStack Query применяет:

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

Однако select может как улучшить, так и ухудшить ситуацию:

Хороший сценарий

select: (data) => data.items

Если items стабилен — ререндеров меньше.

Плохой сценарий

select: (data) => data.items.map(x => ({ ...x }))

Каждый вызов создаёт новые объекты → ссылки всегда новые → ререндеры неизбежны.


Локальная селекция против глобальной нормализации

Без select часто приходится нормализовать данные в глобальном сторе или на уровне компонента:

  • фильтрация внутри useMemo
  • вычисления внутри компонента
  • повторные проходы по массивам

С select эти операции переносятся в слой запроса:

const { data: activeUsers } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  select: (users) => users.filter(u => u.active)
})

Это снижает нагрузку на React-компоненты и уменьшает количество вычислений при каждом рендере.


Комбинация select и queryKey

Важно учитывать, что select не влияет на кэширование напрямую. Кэш в TanStack Query привязан к queryKey, а не к результату select.

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

  • разные select не создают разные кэши
  • один и тот же запрос может иметь разные проекции в разных компонентах

Пример:

useQuery({
  queryKey: ['user', id],
  queryFn: fetchUser,
  select: (user) => user.name
})

useQuery({
  queryKey: ['user', id],
  queryFn: fetchUser,
  select: (user) => user.posts
})

Оба используют один кэш, но разные формы данных.


Оптимизация через изоляцию подписок

Каждый useQuery подписывается на изменения результата после select. Это означает:

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

В сложных интерфейсах это критично:

  • таблицы
  • списки
  • дашборды
  • виртуализированные списки

Сравнение поведения с и без select

Без select

const { data } = useQuery({
  queryKey: ['orders'],
  queryFn: fetchOrders
})

Компонент получает:

  • полный объект
  • любые изменения в объекте могут вызвать ререндер
  • дополнительная работа внутри render-фазы

С select

const { data } = useQuery({
  queryKey: ['orders'],
  queryFn: fetchOrders,
  select: (orders) => orders.map(o => o.id)
})

Компонент получает:

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

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

Использование select влияет на три уровня:

1. React render layer

  • меньше данных → меньше виртуального DOM diff

2. JavaScript execution

  • меньше вычислений в компонентах

3. Memory footprint

  • меньше промежуточных объектов

Ограничения и скрытые риски

Несмотря на преимущества, select имеет особенности:

  • легко создать избыточные пересоздания объектов
  • сложные селекторы могут стать узким местом
  • неправильная мемоизация приводит к деградации производительности

Типичная ошибка:

select: (data) => ({
  items: data.items.filter(x => x.visible)
})

Каждый вызов создаёт новый объект → стабильность теряется.


Стратегии правильного использования select

Эффективные подходы:

  • возвращать примитивы, где возможно
  • избегать глубокого копирования объектов
  • минимизировать трансформации внутри select
  • использовать его как слой проекции, а не бизнес-логики

select как инструмент архитектуры данных

В связке с TanStack Query select становится не просто оптимизацией, а архитектурным инструментом:

  • формирует контракт данных для UI
  • изолирует компоненты от структуры API
  • уменьшает связанность компонентов с backend-ответом
  • стабилизирует рендер-поведение приложения

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