Angular Front-end Инженер

Angular Front-end Инженер

Роадмап навыков для прокачки

ngOnInit и ngOnDestroy

AngularComponentsComponent Lifecycle

Основная идея

ngOnInit и ngOnDestroy — два самых важных lifecycle hook в Angular. Первый используется для инициализации компонента после установки входных свойств, второй — для очистки ресурсов перед уничтожением.

Ключевые аспекты

  • ngOnInit — вызывается один раз после первого ngOnChanges
  • ngOnDestroy — вызывается непосредственно перед удалением компонента из DOM
  • Интерфейсы — реализуются через implements OnInit, OnDestroy
  • @Input доступность — в ngOnInit все входные свойства уже инициализированы
  • Очистка — в ngOnDestroy отписываемся от Observable, очищаем таймеры

Типичные сценарии использования

  • ngOnInit: загрузка данных с сервера, подписка на Observable, инициализация форм
  • ngOnDestroy: отписка от подписок, очистка setInterval/setTimeout, удаление event listeners

Частые ошибки на собеседованиях

  • Инициализация логики в constructor вместо ngOnInit
  • Забывают реализовать интерфейс OnInit / OnDestroy
  • Не отписываются от подписок — приводит к утечкам памяти
  • Пытаются получить @Input в constructor, где он ещё undefined

Введение и проблематика

ngOnInit и ngOnDestroy — фундаментальные lifecycle hooks в Angular. Они решают две ключевые задачи: правильная инициализация компонента после получения входных данных и корректное освобождение ресурсов перед удалением.

Эти два хука используются в 90% компонентов и являются основой для построения надёжных Angular-приложений.

Почему это важно?

  • Правильная инициализация — гарантия, что данные @Input доступны
  • Предотвращение утечек памяти — корректная очистка подписок и таймеров
  • Предсказуемое поведение — код выполняется в нужный момент жизненного цикла

ngOnInit

Определение и назначение

ngOnInit — метод интерфейса OnInit, вызываемый Angular один раз после первого ngOnChanges. Это идеальное место для инициализации компонента.

typescript
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

typescript
// Constructor — только для Dependency Injection
export class MyComponent {
  constructor(
    private http: HttpClient,
    private route: ActivatedRoute,
    private store: Store
  ) {
    // Только сохраняем зависимости
    // Никакой логики инициализации!
  }
}
⚠️

Реализация интерфейса OnInit необязательна, но рекомендуется — она помогает TypeScript проверять правильность сигнатуры метода.


ngOnDestroy

Определение и назначение

ngOnDestroy — метод интерфейса OnDestroy, вызываемый перед тем, как Angular удалит компонент из DOM. Критически важен для освобождения ресурсов.

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


Практические примеры

Паттерн с массивом подписок

typescript
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 (рекомендуемый)

typescript
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

typescript
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-запроса

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

typescript
@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 родителя.


Источники