В чем проблема с отпиской от анонимных функций при подписке и как ее решить?
Что проверяют
Понимаете ли вы, что removeEventListener ищет обработчик по ссылке, а не по тексту функции. Следом спрашивают, как это выглядит в React и какие способы отписки есть кроме хранения ссылки.
Как отвечать
Суть: слушатель определяется типом события, ссылкой на функцию и флагом capture. Каждое выражение () => {...} создаёт новый объект функции, поэтому removeEventListener('click', () => handle()) передаёт другую функцию и ничего не удаляет. Ошибки при этом нет, вызов молча ничего не делает. Обработчик остаётся висеть, держит в замыкании компонент или данные, мешает сборщику мусора, а при повторных подписках срабатывает несколько раз.
Способы решения:
- сохранить ссылку: именованная функция или функция в переменной, одна и та же в
addиremove. Сbindто же самое: результат нужно сохранить, повторный вызов вернёт новую функцию; AbortController: передать{ signal }вaddEventListener, потом вызватьcontroller.abort(). Один сигнал снимает сразу все подписки, анонимные тоже;{ once: true }: для одноразовых обработчиков, отписка происходит сама;- функция отписки: многие API возвращают её сами, как
subscribe()в Redux и Zustand илиunsubscribe()у подписки RxJS. Потоки RxJS удобно завершать черезtakeUntil, в Angular черезtakeUntilDestroyed; - React: подписка в
useEffect, отписка в функции очистки. Обработчик объявляют внутри эффекта, тогда в очистке та же ссылка. В разработкеStrictModeпрогоняет эффект с очисткой лишний раз и быстро показывает забытую отписку.
Флаг capture должен совпадать: подписка с { capture: true } снимается только вызовом с тем же значением, остальные опции на поиск не влияют.
Где спотыкаются
Пишут в очистке эффекта window.removeEventListener('resize', () => onResize()) и уверены, что отписались.
Говорят, что проблему решает lodash. Он даёт debounce и throttle, но обёрнутую функцию всё равно нужно сохранить, иначе снять её не получится.
Делают стабильную ссылку через useCallback с пустым массивом зависимостей и получают обработчик с устаревшими значениями из первого рендера.