Как добиться срабатывания 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.