Методи життєвого циклу компонентів в Angular
Хуки життєвого циклу Angular (Angular component lifecycle hooks) - це методи, які Angular викликає в певні моменти створення, оновлення та видалення компонента з DOM.
Теорія
Коротко
- Хуки спрацьовують у фіксованому порядку:
ngOnChanges→ngOnInit→ ... →ngOnDestroy - Аналогія: будівельний чеклист - фундамент (
ngOnInit), перевірка проводки (ngAfterViewInit), фінальне прибирання (ngOnDestroy) ngOnInit- для завантаження даних,ngOnDestroy- для очищення (підписки, таймери)ngOnChangesспрацьовує доngOnInitі при кожній наступній зміні@InputngDoCheckзапускається при кожному циклі виявлення змін - використовуй тільки як крайній захід
Швидкий приклад
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
@Component({
selector: 'app-user',
template: `<p>{{ user?.name }}</p>`
})
export class UserComponent implements OnInit, OnDestroy {
@Input() user: { name: string };
ngOnInit() {
// Запускається один раз після прив'язки @Input
console.log('Компонент готовий, користувач:', this.user);
}
ngOnDestroy() {
// Запускається перед видаленням з DOM
console.log('Очищення');
}
}
// При створенні: "Компонент готовий, користувач: {name: 'Alice'}"
// При знищенні: "Очищення"В ngOnInit значення @Input вже доступні. В конструкторі - ні.
Порядок виклику хуків
Хуки спрацьовують у такій послідовності при ініціалізації компонента:
| Хук | Коли спрацьовує | Типове використання |
|---|---|---|
ngOnChanges | При прив'язці або зміні @Input | Реакція на зміни від батьківського компонента |
ngOnInit | Після першого ngOnChanges | Запити до API, ініціалізація сервісів |
ngDoCheck | Кожен цикл виявлення змін | Кастомні перевірки рівності |
ngAfterContentInit | Спроектований контент (ng-content) готовий | Запит проектованих компонентів |
ngAfterContentChecked | Перевірка контенту завершена | Логіка після мутацій контенту |
ngAfterViewInit | Компонент і дочірні view відрендерені | Доступ до ViewChild, запуск анімацій |
ngAfterViewChecked | Перевірка view завершена | Донастройка після повного рендеру |
ngOnDestroy | Перед видаленням з DOM | Очищення підписок та таймерів |
Після першого повного проходу Angular циклічно запускає ngDoCheck, ngAfterContentChecked і ngAfterViewChecked при кожному наступному виявленні змін.
Головна різниця: ngOnInit проти конструктора
Конструктор запускається під час ін'єкції залежностей, до того як Angular прив'язує будь-які inputs. ngOnInit запускається після першого проходу виявлення змін, коли значення @Input вже встановлені. Запит до API в конструкторі означає, що виклик сервісу відбудеться без жодного контексту від батьківського компонента. Це найпоширеніша помилка в Angular code review.
Коли використовувати кожен хук
- Запит даних або виклик сервісу:
ngOnInit - Реакція на зміни
@Inputвід батька:ngOnChangesз перевіркою черезSimpleChanges - Доступ до
ViewChildабо запуск DOM-анімацій:ngAfterViewInit - Запит дочірніх компонентів з
ng-content:ngAfterContentInit - Очищення (відписка, скасування таймерів, закриття WebSocket):
ngOnDestroy - Кастомне виявлення змін при мутаціях в OnPush:
ngDoCheck(тільки крайній випадок)
ngOnInit і ngOnDestroy покривають 90% реальних сценаріїв. Content і view хуки потрібні тільки при роботі з ng-content або ViewChild.
Як Angular виконує хуки всередині
Ivy-рендерер (за замовчуванням з Angular 9) створює екземпляр компонента, прив'язує @Input через property setters (це викликає ngOnChanges), потім запускає виявлення змін через Zone.js. Хуки спрацьовують коли рендерер обходить дерево компонентів зверху вниз. Фази після рендеру, наприклад ngAfterViewInit, стоять у черзі мікрозадач після того як view повністю побудований. Ivy генерує виклики хуків на етапі компіляції, що зменшує накладні витрати порівняно зі старим ViewEngine.
Для дерева батьківський-дочірній: спочатку спрацьовує ngOnInit батьківського компонента, потім дочірній проходить повний цикл. ngAfterViewInit батьківського спрацьовує лише після того як всі дочірні завершили ініціалізацію view.
Типові помилки
Запит даних у конструкторі:
// Неправильно - значення @Input ще не прив'язані
constructor(private svc: DataService) {
this.svc.getData(this.userId); // this.userId поки undefined
}
// Правильно
ngOnInit() {
this.svc.getData(this.userId); // @Input() userId вже прив'язаний
}Доступ до ViewChild до ngAfterViewInit:
// Неправильно - view ще не відрендерено
ngOnInit() {
console.log(this.myElement.nativeElement); // null
}
// Правильно
ngAfterViewInit() {
console.log(this.myElement.nativeElement); // працює
}Відсутність відписки в ngOnDestroy:
// Неправильно - підписка живе вічно, витік пам'яті при навігації
ngOnInit() {
interval(1000).subscribe(v => this.data = v);
}
// Правильно - патерн takeUntil
private destroy$ = new Subject<void>();
ngOnInit() {
interval(1000).pipe(takeUntil(this.destroy$)).subscribe(v => this.data = v);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}ngOnChanges без перевірки який input змінився:
// Запускає повне оновлення при будь-якій зміні
ngOnChanges() {
this.loadData();
}
// Перевір спочатку
ngOnChanges(changes: SimpleChanges) {
if (changes['userId'] && !changes['userId'].firstChange) {
this.loadData();
}
}Де зустрічається в реальних проектах
- Angular Material діалоги:
ngAfterViewInitзапускає анімації після того як контент модалки з'явився в DOM - NGRX/NGXS:
ngOnDestroyочищує селектори та ефекти, щоб уникнути застарілих підписок - PrimeNG таблиці:
ngAfterViewCheckedобробляє зміну розміру при virtual scroll після кожного рендер-циклу - Компоненти на основі роутера:
ngOnInitзавантажує дані після того як resolver завершив роботу - OnPush компоненти з мутабельними масивами:
ngDoCheck+markForCheck()коли перевірка за посиланням не спрацьовує
Питання на співбесіді
Q: Яка різниця між конструктором і ngOnInit?
A: Конструктор запускається під час DI, до прив'язки будь-яких @Input. ngOnInit запускається після першого виявлення змін, коли всі inputs вже доступні. Конструктор - тільки для ін'єкції залежностей.
Q: Коли ngOnChanges спрацьовує відносно ngOnInit?
A: ngOnChanges завжди спрацьовує першим, до ngOnInit, при кожній зміні @Input. Якщо компонент не має жодного @Input, ngOnChanges не спрацює ніколи.
Q: Чому ngDoCheck - крайній захід?
A: Він запускається при кожному циклі виявлення змін, навіть якщо нічого не змінилось. Це сотні викликів на секунду. Краще використовувати OnPush з незмінними даними або signals (Angular v16+) перед тим як братись за ngDoCheck.
Q: Як працює порядок хуків у дереві батьківський-дочірній?
A: Спочатку ngOnInit батьківського компонента, потім дочірній проходить повний цикл. ngAfterViewInit батьківського спрацьовує лише після того як всі дочірні завершили ініціалізацію view.
Q: OnPush компонент з мутабельним масивом в @Input - чому UI перестає оновлюватись і як виправити?
A: OnPush пропускає виявлення змін якщо посилання на input не змінилось. Мутація масиву через .push() або .splice() залишає те ж посилання, тому Angular ігнорує компонент. Рішення: ngDoCheck + cdRef.markForCheck(), або краще - замінюй масив новим посиланням при кожному оновленні.
Приклади
Базовий: відстеження зміни @Input через ngOnChanges
@Component({
selector: 'app-counter',
template: '<button (click)="increment()">Рахунок: {{ count }}</button>'
})
export class CounterComponent implements OnChanges {
@Input() max = 10;
count = 0;
ngOnChanges(changes: SimpleChanges) {
if (changes['max']) {
// Реагуємо тільки на конкретний input що змінився
console.log('Max змінено на', changes['max'].currentValue);
if (this.count > changes['max'].currentValue) {
this.count = 0; // скидаємо якщо перевищує нове значення
}
}
}
increment() { this.count++; }
}
// Батьківський компонент змінює @Input max з 10 на 5 → "Max змінено на 5"
// Якщо count був 7, скидається до 0SimpleChanges дає currentValue і previousValue для кожного зміненого input. Без нього ти реагуєш наосліп на кожен цикл виявлення змін.
Середній: завантаження даних з очищенням у компоненті деталей
Цей патерн покриває найтиповіший продакшн-сценарій: запит при ініціалізації, скасування при знищенні. Поширений у Angular Tour of Heroes і схожих архітектурах.
@Component({
template: `
<div *ngIf="hero$ | async as hero">
{{ hero.name }}
</div>
`
})
export class HeroDetailComponent implements OnInit, OnDestroy {
hero$: Observable<Hero>;
private destroy$ = new Subject<void>();
constructor(
private heroService: HeroService,
private route: ActivatedRoute
) {}
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
this.hero$ = this.heroService.getHero(id).pipe(
takeUntil(this.destroy$)
);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete(); // запобігає витоку пам'яті при навігації
}
}
// Герой завантажується при ініціалізації
// Підписка скасовується коли користувач іде зі сторінкиПросунутий: ngDoCheck з OnPush для виявлення мутацій масиву
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<ul><li *ngFor="let item of items">{{ item }}</li></ul>`
})
export class ItemListComponent implements DoCheck {
@Input() items: string[] = [];
private prevLength = 0;
constructor(private cdRef: ChangeDetectorRef) {}
ngDoCheck() {
// OnPush пропускає array.push() бо посилання не змінюється
if (this.items.length !== this.prevLength) {
this.prevLength = this.items.length;
this.cdRef.markForCheck(); // примушуємо Angular переперевірити це піддерево
}
}
}
// Без ngDoCheck: UI не оновлюється після array.push()
// З ним: оновлення при зміні довжини, але виклик іде при кожному циклі виявлення змін
// Краща альтернатива: замінювати масив новим посиланням при кожному оновленніВ продакшні я віддаю перевагу незмінним оновленням масиву над ngDoCheck. Патерн з markForCheck працює, але кожна додаткова логіка в ngDoCheck рано чи пізно стає темою розмови про продуктивність на code review.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.