Введение и проблематика
ngOnInit и ngOnDestroy — фундаментальные lifecycle hooks в Angular. Они решают две ключевые задачи: правильная инициализация компонента после получения входных данных и корректное освобождение ресурсов перед удалением.
Эти два хука используются в 90% компонентов и являются основой для построения надёжных Angular-приложений.
Почему это важно?
Правильная инициализация — гарантия, что данные @Input доступны
Предотвращение утечек памяти — корректная очистка подписок и таймеров
Предсказуемое поведение — код выполняется в нужный момент жизненного цикла
ngOnInit
Определение и назначение
ngOnInit — метод интерфейса OnInit, вызываемый Angular один раз после первого ngOnChanges. Это идеальное место для инициализации компонента.
import { Component , OnInit , Input } from '@angular/core' ;
@ Component ({
selector : 'app-user-profile' ,
template : `
<div *ngIf="user">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
`
})
export class UserProfileComponent implements OnInit {
@ Input () userId !: string ;
user : User | null = null ;
constructor ( private userService : UserService ) {
// ❌ userId здесь undefined!
}
ngOnInit () : void {
// ✅ userId уже доступен
this . userService .getUser ( this .userId) .subscribe (user => {
this .user = user;
});
}
}
Когда использовать ngOnInit
Сценарий Пример Загрузка данных HTTP-запросы к API Подписка на Observable Подписка на данные из сервиса Инициализация форм Создание FormGroup с начальными значениями Настройка параметров Инициализация на основе @Input
Constructor vs ngOnInit
Constructor ngOnInit
// Constructor — только для Dependency Injection
export class MyComponent {
constructor (
private http : HttpClient ,
private route : ActivatedRoute ,
private store : Store
) {
// Только сохраняем зависимости
// Никакой логики инициализации!
}
}
⚠️
Реализация интерфейса OnInit необязательна, но рекомендуется — она помогает TypeScript проверять правильность сигнатуры метода.
ngOnDestroy
Определение и назначение
ngOnDestroy — метод интерфейса OnDestroy, вызываемый перед тем, как Angular удалит компонент из DOM. Критически важен для освобождения ресурсов.
import { Component , OnInit , OnDestroy } from '@angular/core' ;
import { Subscription , interval } from 'rxjs' ;
@ Component ({
selector : 'app-live-clock' ,
template : `<p>{{ currentTime | date:'HH:mm:ss' }}</p>`
})
export class LiveClockComponent implements OnInit , OnDestroy {
currentTime = new Date ();
private timerSubscription !: Subscription ;
ngOnInit () : void {
// Запускаем интервал обновления
this .timerSubscription = interval ( 1000 ) .subscribe (() => {
this .currentTime = new Date ();
});
}
ngOnDestroy () : void {
// Обязательно отписываемся!
if ( this .timerSubscription) {
this . timerSubscription .unsubscribe ();
}
}
}
Что нужно очищать в ngOnDestroy
Ресурс Почему важно очистить Subscription (RxJS) Продолжит работать и вызывать callback setInterval / setTimeout Будет выполняться после уничтожения Event listeners Может вызывать методы уничтоженного компонента WebSocket соединения Будет держать открытое соединение
🚫
Утечки памяти — одна из самых распространённых проблем в Angular. Всегда отписывайтесь в ngOnDestroy!
Практические примеры
Паттерн с массивом подписок
import { Component , OnInit , OnDestroy } from '@angular/core' ;
import { Subscription } from 'rxjs' ;
@ Component ({
selector : 'app-dashboard' ,
template : `<!-- template -->`
})
export class DashboardComponent implements OnInit , OnDestroy {
private subscriptions : Subscription [] = [];
constructor (
private userService : UserService ,
private notificationService : NotificationService
) {}
ngOnInit () : void {
// Собираем все подписки в массив
this . subscriptions .push (
this . userService . currentUser$ .subscribe (user => {
console .log ( 'User:' , user);
})
);
this . subscriptions .push (
this . notificationService . notifications$ .subscribe (notification => {
console .log ( 'Notification:' , notification);
})
);
}
ngOnDestroy () : void {
// Отписываемся от всех сразу
this . subscriptions .forEach (sub => sub .unsubscribe ());
}
}
Паттерн с takeUntil (рекомендуемый)
import { Component , OnInit , OnDestroy } from '@angular/core' ;
import { Subject } from 'rxjs' ;
import { takeUntil } from 'rxjs/operators' ;
@ Component ({
selector : 'app-orders' ,
template : `<ul><li *ngFor="let order of orders">{{ order.id }}</li></ul>`
})
export class OrdersComponent implements OnInit , OnDestroy {
orders : Order [] = [];
private destroy$ = new Subject < void >();
constructor ( private orderService : OrderService ) {}
ngOnInit () : void {
// takeUntil автоматически отпишется при emit в destroy$
this . orderService .getOrders ()
.pipe ( takeUntil ( this .destroy$))
.subscribe (orders => {
this .orders = orders;
});
this . orderService .orderUpdates$
.pipe ( takeUntil ( this .destroy$))
.subscribe (update => {
console .log ( 'Order updated:' , update);
});
}
ngOnDestroy () : void {
// Один emit завершает все подписки с takeUntil
this . destroy$ .next ();
this . destroy$ .complete ();
}
}
Очистка DOM event listeners
import { Component , OnInit , OnDestroy , ElementRef } from '@angular/core' ;
@ Component ({
selector : 'app-scroll-tracker' ,
template : `<p>Scroll position: {{ scrollY }}</p>`
})
export class ScrollTrackerComponent implements OnInit , OnDestroy {
scrollY = 0 ;
private scrollHandler = this . onScroll .bind ( this );
ngOnInit () : void {
window .addEventListener ( 'scroll' , this .scrollHandler);
}
ngOnDestroy () : void {
window .removeEventListener ( 'scroll' , this .scrollHandler);
}
private onScroll () : void {
this .scrollY = window .scrollY;
}
}
Пограничные кейсы
Компонент уничтожается до завершения HTTP-запроса
@ Component ({
selector : 'app-slow-loader' ,
template : `<p>{{ data }}</p>`
})
export class SlowLoaderComponent implements OnInit , OnDestroy {
data = '' ;
private destroy$ = new Subject < void >();
constructor ( private http : HttpClient ) {}
ngOnInit () : void {
this . http .get < string >( '/api/slow-endpoint' )
.pipe ( takeUntil ( this .destroy$))
.subscribe (data => {
// Не выполнится, если компонент уничтожен
this .data = data;
});
}
ngOnDestroy () : void {
this . destroy$ .next ();
this . destroy$ .complete ();
}
}
ngOnDestroy не вызывается при закрытии вкладки
⚠️
ngOnDestroy НЕ вызывается при закрытии вкладки браузера или перезагрузке страницы. Для таких случаев используйте window.onbeforeunload.
@ Component ({
selector : 'app-form' ,
template : `<form>...</form>`
})
export class FormComponent implements OnInit , OnDestroy {
private beforeUnloadHandler = (e : BeforeUnloadEvent ) => {
if ( this .hasUnsavedChanges) {
e .preventDefault ();
e .returnValue = '' ;
}
};
ngOnInit () : void {
window .addEventListener ( 'beforeunload' , this .beforeUnloadHandler);
}
ngOnDestroy () : void {
window .removeEventListener ( 'beforeunload' , this .beforeUnloadHandler);
}
}
Вопросы интервьюера
Q: Зачем реализовывать интерфейс OnInit, если метод ngOnInit и так будет вызван?
Интерфейс обеспечивает проверку типов TypeScript — если допустите опечатку в имени метода, компилятор сообщит об ошибке.
Q: Что произойдёт, если не отписаться от Observable в ngOnDestroy?
Подписка продолжит работать, callback будет вызываться даже для уничтоженного компонента. Это приводит к утечкам памяти и потенциальным ошибкам.
Q: Можно ли использовать async/await в ngOnInit?
Да, ngOnInit может быть async-функцией, но Angular не будет ждать её завершения. Ошибки нужно обрабатывать самостоятельно через try/catch.
Q: В каком порядке вызываются хуки родителя и ребёнка?
ngOnInit родителя → ngOnInit ребёнка. При уничтожении: ngOnDestroy ребёнка → ngOnDestroy родителя.
Источники