Skip to main content

Методи життєвого циклу компонентів в Angular

Хуки життєвого циклу Angular (Angular component lifecycle hooks) - це методи, які Angular викликає в певні моменти створення, оновлення та видалення компонента з DOM.

Теорія

Коротко

  • Хуки спрацьовують у фіксованому порядку: ngOnChanges → ngOnInit → ... → ngOnDestroy
  • Аналогія: будівельний чеклист - фундамент (ngOnInit), перевірка проводки (ngAfterViewInit), фінальне прибирання (ngOnDestroy)
  • ngOnInit - для завантаження даних, ngOnDestroy - для очищення (підписки, таймери)
  • ngOnChanges спрацьовує до ngOnInit і при кожній наступній зміні @Input
  • ngDoCheck запускається при кожному циклі виявлення змін - використовуй тільки як крайній захід

Швидкий приклад

typescript
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.

Типові помилки

Запит даних у конструкторі:

typescript
// Неправильно - значення @Input ще не прив'язані constructor(private svc: DataService) { this.svc.getData(this.userId); // this.userId поки undefined } // Правильно ngOnInit() { this.svc.getData(this.userId); // @Input() userId вже прив'язаний }

Доступ до ViewChild до ngAfterViewInit:

typescript
// Неправильно - view ще не відрендерено ngOnInit() { console.log(this.myElement.nativeElement); // null } // Правильно ngAfterViewInit() { console.log(this.myElement.nativeElement); // працює }

Відсутність відписки в ngOnDestroy:

typescript
// Неправильно - підписка живе вічно, витік пам'яті при навігації 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 змінився:

typescript
// Запускає повне оновлення при будь-якій зміні 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

typescript
@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, скидається до 0

SimpleChanges дає currentValue і previousValue для кожного зміненого input. Без нього ти реагуєш наосліп на кожен цикл виявлення змін.

Середній: завантаження даних з очищенням у компоненті деталей

Цей патерн покриває найтиповіший продакшн-сценарій: запит при ініціалізації, скасування при знищенні. Поширений у Angular Tour of Heroes і схожих архітектурах.

typescript
@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 для виявлення мутацій масиву

typescript
@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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.