PassFront
  1. PassFront
  2. База знаний
  3. Хуки

Как добиться срабатывания useEffect при мутировании объекта в зависимостях?

Хуки

Короткий ответ: никак — и не нужно. Мутация не меняет ссылку, поэтому сравнение deps (Object.is) её не видит. Правильное решение — не «заставить эффект видеть мутации», а перестать мутировать.

Почему эффект не срабатывает

const [filters, setFilters] = useState({ status: 'active' });

filters.status = 'done';        // ссылка не изменилась
setFilters(filters);            // Object.is(prev, next) → рендер пропущен

useEffect(() => {
  load(filters);
}, [filters]);                  // deps «не изменились» — эффект молчит

Двойной провал: сеттер с той же ссылкой не вызывает рендер, а без рендера deps даже не сравниваются.

Правильное решение: иммутабельные обновления

setFilters((prev) => ({ ...prev, status: 'done' })); // новая ссылка
// рендер происходит, deps видят новый объект, эффект срабатывает

Для глубоких структур — Immer (produce), для массивов — map/filter/spread.

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

(например, инстанс внешней библиотеки)

  • Зависеть от примитивов: [filters.status, filters.page] — сравнение по значениям полей (но сами поля обновлять всё равно нужно с рендером);
  • Счётчик версий: рядом с мутируемым объектом держать const [version, setVersion] = useState(0); после мутации — setVersion((v) => v + 1), эффект зависит от version;
  • useSyncExternalStore — если объект живёт вне React и умеет уведомлять об изменениях (подписка + getSnapshot).

Все они — костыли по сравнению с иммутабельностью; счётчик версий оправдан только для интеграций.

Коротко для собеседования

Мутации невидимы для deps: ссылка та же, Object.is молчит, а сеттер с тем же объектом даже рендер не запускает. Правильно — обновлять иммутабельно (spread/Immer): новая ссылка триггерит и рендер, и эффект. Для немутируемых извне объектов — зависимости-примитивы, счётчик версий или useSyncExternalStore.

Другие вопросы раздела