Skip to main content

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

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

Теорія

Коротко

  • Хуки спрацьовують у фіксованому порядку: ngOnChangesngOnInit → ... → 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

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

Дочитали статтю?