Angular
effect: побочные эффекты
Приложение должно сохранять выбранную тему оформления в localStorage при каждом её изменении, а ещё логировать смену в аналитику. Это не вычисление нового значения - это действие во внешнем мире, побочный эффект. computed для такого не подходит: он обязан быть чистым и лишь возвращать значение. Здесь нужен effect - конструкция, которая автоматически запускает заданный код при изменении прочитанных сигналов. Но у effect есть строгие правила: где его создавать, как чистить ресурсы и почему из него почти никогда не стоит писать в сигналы.
- Синхронизация с localStorage или sessionStorage при изменении пользовательских настроек
- Логирование и аналитика: отправка события при смене состояния (выбор фильтра, открытие модального окна)
- Императивная работа с не-Angular API: обновление заголовка вкладки, синхронизация с canvas-библиотекой
- Отладка: вывод в консоль текущих значений сигналов при разработке реактивных компонентов
- Управление сторонними виджетами: перерисовка карты или графика при изменении входных сигналов
Предварительные знания
- Понимание signal и computed: чтение, запись, отслеживание зависимостей
- Идея побочного эффекта: действие, меняющее состояние вне функции (запись в хранилище, сеть, DOM)
- Базовое представление о контексте инъекции в Angular
Зачем effect отделили от computed
В реактивных системах исторически различают два вида производной реакции на изменения. Первый - вычисление нового значения без побочных эффектов (в Angular это computed). Второй - выполнение действия во внешнем мире (это effect). Разделение пришло из MobX, где есть computed и reaction/autorun, и из похожих библиотек. Команда Angular намеренно сделала границу строгой: computed обязан быть чистым, а любые побочные эффекты выносятся в effect. effect вышел вместе с сигналами в Angular 16, оставался стабильным к версии 17, и к Angular 21 закрепилось руководство: использовать effect редко и осторожно, не записывая из него сигналы, чтобы не создавать запутанных циклов реактивности.
effect: реакция на изменения с побочным действием
effect принимает функцию и выполняет её сразу при создании, а затем повторно при каждом изменении любого сигнала, прочитанного внутри. Отслеживание зависимостей работает так же, как в computed: учитываются сигналы, реально прочитанные во время выполнения. Разница в назначении: computed вычисляет и возвращает значение, а effect ничего не возвращает и существует ради действия во внешнем мире.
При создании компонента effect выполнится один раз и запишет начальную тему. Затем каждый вызов theme.set перезапустит effect, и новое значение попадёт в localStorage. Никакого ручного вызова не требуется - effect сам реагирует на изменение сигнала theme, который он читает.
effect нужен заметно реже, чем computed. Большинство задач, которые кажутся побочными эффектами, на деле выражаются через производные значения. effect уместен только там, где действительно происходит выход за пределы реактивной системы: запись в хранилище, сеть, императивный DOM, логирование.
В чём ключевое различие между effect и computed?
Очистка через onCleanup и контекст инъекции
Если effect создаёт ресурс - таймер, обработчик события, временную подписку - этот ресурс нужно освобождать перед следующим запуском и при остановке effect. Для этого функция effect получает аргумент onCleanup: в него передают функцию очистки, которую Angular вызовет перед повторным выполнением и при уничтожении effect.
Здесь при каждом изменении intervalMs старый таймер очищается через onCleanup, и создаётся новый с актуальным интервалом. Без очистки накопились бы дублирующиеся таймеры. Тот же колбэк сработает при удалении компонента, поэтому утечки не произойдёт.
Второй важный момент - где создавать effect. По умолчанию его регистрируют в контексте инъекции: в конструкторе компонента или сервиса либо в инициализаторе поля. Так effect получает доступ к inject и привязывается к жизненному циклу владельца, останавливаясь автоматически при его удалении. Создание effect вне контекста инъекции потребует явной передачи injector.
Создание effect внутри ngOnInit или обработчика события без передачи injector приведёт к ошибке о контексте инъекции. Самое надёжное место - конструктор или инициализатор поля компонента, где контекст доступен по умолчанию.
Зачем effect передают onCleanup при работе с таймером через setInterval?
Почему не стоит писать сигналы из effect
Соблазн велик: прочитать один сигнал в effect и записать другой, чтобы получить зависимое состояние. Но это антипаттерн. Запись сигнала из effect создаёт неявный каскад: изменение исходного запускает effect, тот меняет другой сигнал, который может снова что-то запустить. Логика становится трудной для понимания, а в худшем случае возникает бесконечный цикл. Поэтому Angular по умолчанию запрещает запись сигналов внутри effect.
- Нужно производное значение — Используется computed. Значение вычисляется из источников, чисто и предсказуемо, без записи сигналов
- Нужно зависимое, но перезаписываемое состояние — Используется linkedSignal. Сбрасывается от источника, но допускает ручную запись, без побочных каскадов
Правило простое. Если нужно вывести значение из других сигналов - это computed. Если нужно зависимое состояние, которое иногда перезаписывают вручную - это linkedSignal из следующего урока. И только если нужно действие во внешнем мире (хранилище, сеть, DOM, лог) - это effect. Запись сигнала из effect остаётся редким исключением для особых случаев, и Angular требует явного флага allowSignalWrites для этого.
Хорошая проверка перед использованием effect: спросить, не пытается ли он вычислить производное значение. Если да, это сигнал заменить его на computed или linkedSignal. effect оставляют для настоящих побочных эффектов.
Нужно держать значение doubled, всегда равное удвоенному source. Какой инструмент правильный?
Связь с другими темами
effect замыкает базовую тройку сигнальной модели вместе с signal и computed:
- Сигналы — effect читает сигналы и перезапускается при их изменении
- computed — Для производного значения берут computed, для действия во внешнем мире - effect
- linkedSignal — Когда хочется записать сигнал в ответ на изменение источника, обычно правильнее linkedSignal, а не effect
Итог
- effect выполняет переданную функцию при создании и при каждом изменении прочитанных внутри сигналов
- Зависимости отслеживаются автоматически, как у computed, но effect не возвращает значение, а производит побочный эффект
- Через колбэк onCleanup внутри effect освобождают ресурсы (таймеры, подписки) перед следующим запуском и при остановке
- effect создаётся в контексте инъекции (в конструкторе или поле компонента) и автоматически останавливается при удалении владельца
- По умолчанию из effect не записывают сигналы: это ведёт к запутанным каскадам, для производного состояния есть computed и linkedSignal
Связанные уроки
- ng-11-signals-intro — effect реагирует на изменение сигналов, прочитанных внутри него
- ng-12-computed — computed тоже отслеживает зависимости, но возвращает значение, а не выполняет побочный эффект
- ng-14-linked-signal — linkedSignal - правильная альтернатива записи сигнала из effect для зависимого состояния