javascript

Современный Angular: Заменяем жизненные циклы на сигналы

  • пятница, 29 мая 2026 г. в 00:00:11
https://habr.com/ru/articles/1040488/

Если вы пишете на Angular, то наверняка часто используете хуки жизненного цикла вроде ngOnChangesngOnInit и ngOnDestroy. С появлением сигналов и концепции Zoneless (когда Zone.js уже не обязателен) у нас появились более элегантные и читаемые альтернативы.

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

1. Вместо ngOnChanges — computed()

Было: классический подход с ngOnChanges

@Component({...})
export class PricingComponent implements OnChanges {
  @Input() price = 0;
  totalPrice = 0;

  constructor(private taxService: TaxService) {}

  ngOnChanges(changes: SimpleChanges) {
    if (changes['price']) {
      // При обновлении инпута дергаем сервис
      this.totalPrice = this.taxService.calculateTotal(this.price);
    }
  }
}

Стало: реактивно с сигналами

@Component({...})
export class PricingComponent {
  // Сигнальный вход
  price = input<number>(0);
  private taxService = inject(TaxService);

  // Реактивно вычисляем общую цену при каждом изменении price
  totalPrice = computed(() => this.taxService.calculateTotal(this.price()));
}

Плюсы: короче, читабельно, более производительно (вычисляется только когда реально нужно).

2. Отказываемся от ngOnInit в пользу декларативной инициализации

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

Было: инициализация в ngOnInit

@Component({...})
export class UserComponent implements OnInit {
  @Input() userId!: string;
  userData!: User;

  constructor(private userService: UserService) {}

  ngOnInit() {
    // Ждем, пока Angular установит userId
    this.userData = this.userService.getUserSync(this.userId);
  }
}

Стало: сигнальный подход

@Component({...})
export class UserComponent {
  userId = input.required<string>(); // обязательный вход
  private userService = inject(UserService);

  // Данные пользователя вычисляются реактивно при каждом изменении userId
  userData = computed(() => this.userService.getUserSync(this.userId()));
}

Дополнительный бонус: теперь при изменении userId (если такое возможно) данные обновятся автоматически — без лишних телодвижений.

3. Убиваем ngOnDestroy с помощью takeUntilDestroyed()

Больше не нужно вручную хранить массивы подписок (Subscription[]) и отписываться в ngOnDestroy. Спасибо оператору takeUntilDestroyed().

Было: ручное управление подпиской

@Component({...})
export class AlertComponent implements OnDestroy {
  private sub: Subscription;

  constructor(private alertService: AlertService) {
    this.sub = this.alertService.alerts$
      .subscribe(alert => console.log(alert));
  }

  ngOnDestroy() {
    this.sub.unsubscribe();
  }
}

Стало: автоматическая отписка через pipe

@Component({...})
export class AlertComponent {
  constructor(private alertService: AlertService) {
    this.alertService.alerts$
      .pipe(takeUntilDestroyed())   // Angular сам отпишет при уничтожении компонента
      .subscribe(alert => console.log(alert));
  }
}

Код стал лаконичнее, и риск забыть про unsubscribe исчезает.(А если совсем перейти с RxJS на сигналы, то можно сделать еще проще — но это уже тема для отдельной статьи.)

Итог: жизненные циклы не умерли, но стали специализированным инструментом

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

Означает ли это, что жизненные циклы полностью уходят в прошлое? Нет. Они остаются для специфических задач — например, ngAfterViewInit всё ещё незаменим, когда нужна прямая работа с DOM или canvas.

Но как инструмент "на каждый день" — да, хуки успешно заменяются современными реактивными примитивами.

А что думаете вы?

Переход на сигналы — это действительно эволюция или просто ещё один "очередной способ писать то же самое"? Сталкивались ли вы с подводными камнями при миграции с жизненных циклов на computed и takeUntilDestroyed?

Особенно интересно услышать мнение тех, кто уже пробовал Zoneless-подход в боевых проектах:

  • Какие грабли успели собрать?

  • В каких кейсах без старых хуков всё-таки не обойтись?

  • Как оцениваете производительность после перехода?

А если есть чем поделиться или возразить — добро пожаловать в комментарии. Живое обсуждение — лучший способ разобраться в новой парадигме.