notifyOnChangeProps

Поведение подписки на изменения в TanStack Query напрямую влияет на количество ререндеров компонентов и общую производительность приложения. Одним из ключевых механизмов тонкой настройки является notifyOnChangeProps, позволяющий контролировать, какие именно изменения данных должны приводить к обновлению компонента.

По умолчанию useQuery подписывается на все изменения результата запроса. Это означает, что любое изменение состояния query-объекта — будь то обновление data, status, isFetching, error или даже метаданных — может вызвать ререндер компонента.

Такая модель проста, но в крупных приложениях приводит к избыточным обновлениям UI. Особенно это заметно в случаях, когда компонент использует только небольшую часть данных, но подписан на весь объект результата.

Типичный пример:

const { data } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
})

Даже если компонент использует только data, он всё равно будет перерендериваться при изменении других полей результата.

Сущность notifyOnChangeProps

notifyOnChangeProps позволяет ограничить список свойств, изменение которых вызывает ререндер компонента.

Возможные значения:

  • 'all' — поведение по умолчанию, подписка на все изменения
  • массив строк — список конкретных полей результата query
  • функция — динамическое определение зависимостей

Базовое использование:

const query = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  notifyOnChangeProps: ['data'],
})

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

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

При использовании массива строк TanStack Query сравнивает изменения только указанных полей. Если изменяется поле, не входящее в список, ререндер не происходит.

Пример:

const query = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  notifyOnChangeProps: ['data', 'error'],
})

Теперь компонент реагирует только на:

  • обновление данных
  • появление или изменение ошибки

Изменения isFetching, status, isLoading игнорируются с точки зрения ререндера.

Важно учитывать, что сама подписка на query остаётся полной — фильтруется только уведомление о необходимости рендера.

Оптимизация с использованием функции

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

const query = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  notifyOnChangeProps: (props, prevProps) => {
    if (props.data !== prevProps.data) {
      return ['data']
    }

    return []
  },
})

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

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

Влияние на архитектуру компонентов

Использование notifyOnChangeProps часто становится инструментом оптимизации в компонентах с высокой частотой обновлений или сложной вложенной структурой UI.

Типичный сценарий:

  • список элементов, где обновляется isFetching при фоновом рефетче
  • компонент, использующий только data
  • необходимость избежать лишнего ререндера списка

Пример:

function UsersList() {
  const { data } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    notifyOnChangeProps: ['data'],
  })

  return (
    <ul>
      {data?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

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

Ограничения подхода

Использование notifyOnChangeProps требует понимания внутренних механизмов TanStack Query.

Ключевые ограничения:

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

Особенно важно учитывать, что игнорирование isFetching или status может скрыть важные состояния загрузки.

Взаимодействие с селекторами

В современных версиях TanStack Query предпочтение часто отдаётся select, который позволяет не только контролировать ререндер, но и трансформировать данные.

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

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

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

Практическое поведение при refetch

Во время повторных запросов TanStack Query может обновлять несколько полей состояния одновременно:

  • isFetching
  • status
  • dataUpdatedAt
  • fetchStatus

При стандартной подписке компонент будет реагировать на каждое из этих изменений. С notifyOnChangeProps: ['data'] ререндер произойдёт только при фактическом изменении data, но не при изменении статуса запроса.

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

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

Оптимизация через notifyOnChangeProps имеет смысл в следующих случаях:

  • большие списки с частыми refetch
  • компоненты с тяжёлым render-логикой
  • приложения с высоким количеством параллельных запросов

В небольших приложениях эффект может быть незаметен, а усложнение логики — избыточным.

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

Часто notifyOnChangeProps используется вместе с React.memo, создавая двойной слой защиты от лишних ререндеров:

const UsersList = React.memo(() => {
  const { data } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    notifyOnChangeProps: ['data'],
  })

  return ...
})

В этом случае:

  • notifyOnChangeProps ограничивает триггеры обновления
  • React.memo предотвращает ререндер от родителя

Такая комбинация усиливает контроль над render pipeline.

Поведение при dev и production

В development-режиме React и TanStack Query могут добавлять дополнительные проверки и поведение, связанное с Strict Mode. Это иногда создаёт впечатление, что notifyOnChangeProps работает менее предсказуемо из-за двойных ререндеров.

В production эти эффекты исчезают, и поведение становится более стабильным.

Сравнение с альтернативными подходами

До появления селекторов и развитой системы оптимизаций использовались:

  • ручное разделение query
  • локальное хранение части данных
  • мемоизация через useMemo и useSelector

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

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