Angular — Жизненный цикл компонента

Жизненный цикл начинается когда Angular создаёт экземпляр компонента и рендерит представление, и заканчивается при уничтожении компонента и удалении его из DOM.


Порядок вызовов хуков

constructor()  
↓  
ngOnChanges() ← вызывается до ngOnInit и при каждом изменении @Input  
↓  
ngOnInit() ← один раз после первого ngOnChanges  
↓  
ngDoCheck() ← каждый цикл Change Detection  
↓  
ngAfterContentInit() ← один раз после проекции ng-content  
↓  
ngAfterContentChecked() ← после каждой проверки контента  
↓  
ngAfterViewInit() ← один раз после инициализации View  
↓  
ngAfterViewChecked() ← после каждой проверки View  
↓  
ngOnDestroy() ← перед уничтожением компонента

Хуки подробно

constructor()

Не является хуком Angular — это конструктор класса TypeScript.

  • Вызывается первым при создании экземпляра
  • @Input()-свойства ещё не заполнены
  • Использовать только для DI (внедрения зависимостей)
  • ❌ Нельзя делать HTTP-запросы, работать с DOM
constructor(private authService: AuthService) {}

ngOnChanges(changes: SimpleChanges)

  • Вызывается до ngOnInit и при каждом изменении @Input()
  • Получает объект SimpleChanges с предыдущими и текущими значениями
  • ❗ Вызывается только если у компонента есть хотя бы один @Input()
ngOnChanges(changes: SimpleChanges): void {
  if (changes['user']) {
    const prev = changes['user'].previousValue;
    const curr = changes['user'].currentValue;
    console.log(`User changed: ${prev?.name} → ${curr?.name}`);
  }
}

ngOnInit()

  • Вызывается один раз после первого ngOnChanges
  • @Input()-свойства уже заполнены
  • Основное место для инициализации: HTTP-запросы, подписки, начальная логика
ngOnInit(): void {
  this.userService.getUser(this.userId).subscribe(user => {
    this.user = user;
  });
}

ngDoCheck()

  • Вызывается при каждом запуске Change Detection (очень часто!)
  • Позволяет реагировать на изменения, которые Angular не отловил сам
  • ⚠️ Использовать осторожно — может сильно снижать производительность
ngDoCheck(): void {
  if (this.user.name !== this.prevName) {
    this.prevName = this.user.name;
    // реакция на изменение
  }
}

ngAfterContentInit()

  • Вызывается один раз после того, как Angular спроецировал внешний контент через <ng-content>
  • Здесь можно безопасно работать с @ContentChild / @ContentChildren
@ContentChild(HeaderComponent) header!: HeaderComponent;
 
ngAfterContentInit(): void {
  console.log('Header:', this.header);
}

ngAfterContentChecked()

  • Вызывается после каждой проверки спроецированного контента
  • Срабатывает сразу после ngAfterContentInit и затем после каждого ngDoCheck
  • ⚠️ Вызывается очень часто

ngAfterViewInit()

  • Вызывается один раз после инициализации представления компонента и всех дочерних компонентов
  • Здесь можно безопасно работать с @ViewChild / @ViewChildren
  • ❗ Нельзя изменять данные, которые влияют на шаблон (вызовет ExpressionChangedAfterItHasBeenCheckedError)
@ViewChild('canvas') canvas!: ElementRef<HTMLCanvasElement>;
 
ngAfterViewInit(): void {
  const ctx = this.canvas.nativeElement.getContext('2d');
  // работа с Canvas
}

ngAfterViewChecked()

  • Вызывается после каждой проверки представления компонента и дочерних компонентов
  • Срабатывает сразу после ngAfterViewInit и затем после каждого цикла CD
  • ⚠️ Вызывается очень часто — минимум логики

ngOnDestroy()

  • Вызывается один раз непосредственно перед уничтожением компонента
  • Обязательное место для очистки ресурсов во избежание утечек памяти
private destroy$ = new Subject<void>();
 
ngOnInit(): void {
  this.data$.pipe(takeUntil(this.destroy$)).subscribe(...);
}
 
ngOnDestroy(): void {
  this.destroy$.next();
  this.destroy$.complete();
  // Отписки, clearInterval, removeEventListener
}

💡 Современная альтернатива — takeUntilDestroyed() из @angular/core/rxjs-interop (Angular 16+), который не требует ручного ngOnDestroy.


Сводная таблица

ХукКоличество вызововКогдаТипичное использование
constructor1При создании классаВнедрение зависимостей
ngOnChangesNПри изменении @Input()Реакция на изменение входных данных
ngOnInit1После первого ngOnChangesHTTP-запросы, инициализация
ngDoCheckNКаждый цикл CDРучная проверка изменений
ngAfterContentInit1После проекции контентаРабота с @ContentChild
ngAfterContentCheckedNПосле проверки контентаРедко используется
ngAfterViewInit1После инициализации ViewРабота с @ViewChild, DOM
ngAfterViewCheckedNПосле проверки ViewРедко используется
ngOnDestroy1Перед уничтожениемОтписки, очистка ресурсов

Интерфейсы

Каждый хук соответствует интерфейсу — рекомендуется явно их реализовывать:

import {
  OnChanges, OnInit, DoCheck,
  AfterContentInit, AfterContentChecked,
  AfterViewInit, AfterViewChecked,
  OnDestroy
} from '@angular/core';
 
@Component({ selector: 'app-example', template: '' })
export class ExampleComponent implements OnInit, OnDestroy {
  ngOnInit(): void { }
  ngOnDestroy(): void { }
}

Полный пример компонента

@Component({
  selector: 'app-user',
  template: `<p>{{ user.name }}</p>`
})
export class UserComponent implements OnChanges, OnInit, OnDestroy {
  @Input() userId!: number;
 
  user!: User;
  private destroy$ = new Subject<void>();
 
  constructor(private userService: UserService) {}
 
  ngOnChanges(changes: SimpleChanges): void {
    if (changes['userId'] && !changes['userId'].firstChange) {
      this.loadUser(); // перезагрузить при смене userId
    }
  }
 
  ngOnInit(): void {
    this.loadUser();
  }
 
  private loadUser(): void {
    this.userService.getUser(this.userId)
      .pipe(takeUntil(this.destroy$))
      .subscribe(u => this.user = u);
  }
 
  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Связанные темы