Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Методи життєвого циклу компонентів в Angular». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Хуки життєвого циклу Angular** (lifecycle hooks) - методи, які Angular викликає при створенні, оновленні та знищенні компонента. ```typescript export class MyComponent implements OnInit, OnDestroy { @Input() id: number; ngOnInit() { /* запускається після прив'язки @Input */ } ngOnDestroy() { /* очищення перед видаленням з DOM */ } } ``` **Головне:** `ngOnInit` для завантаження даних (не конструктор), `ngOnDestroy` для відписок.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Хуки життєвого циклу 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.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.