Angular IDPStrong Junior
🔥 0

Модуль 2 · урок 5 з 15

Dependency Injection

Ін'єкція залежностей — те, через що Angular здається складнішим за React. Насправді ідея одна: компонент каже, що йому потрібно, а не як це створити. Усе інше — деталі реалізації, і їх небагато.

Навіщо це взагалі

// без DI: компонент сам створює те, від чого залежить
export class TaskList {
  private api = new TaskApiService(new HttpClient(), new Logger());
}

Три проблеми в одному рядку. По-перше, компонент мусить знати, як збирати сервіс, — і при зміні конструктора зламаються всі місця. По-друге, кожен компонент отримає свій екземпляр, а значить свій кеш і свій стан. По-третє, у тесті цей сервіс не підміниш — він жорстко зашитий.

// з DI: компонент лише називає потребу
export class TaskList {
  private api = inject(TaskApiService);
}

Тепер Angular сам створює сервіс, сам передає йому його залежності, зберігає один екземпляр на весь застосунок і дає підмінити його в тестах одним рядком. Це і є вся суть.

Як зробити клас доступним для інʼєкції

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })   // один екземпляр на весь застосунок
export class TaskApiService {
  private http = inject(HttpClient);

  getTasks() { return this.http.get<Task[]>('/api/tasks'); }
}

providedIn: 'root' — це те, що ти писатимеш у 95% випадків. Бонус: якщо сервіс ніде не використовується, збирач викине його з бандла — цього не вміє реєстрація через providers вручну.

inject() чи конструктор

// сучасно
export class TaskList {
  private api = inject(TaskApiService);
  private router = inject(Router);
}

// класично — те саме, працює далі
export class TaskList {
  constructor(
    private api: TaskApiService,
    private router: Router,
  ) {}
}

Різниця не косметична. inject() працює й поза класами — у функціональних guards, інтерсепторах, резолверах і у власних функціях. Саме тому нові API Angular написані під нього. Обмеження одне: викликати inject() можна лише в контексті інʼєкції — тобто в ініціалізаторі поля, у конструкторі або всередині runInInjectionContext(). Виклик із ngOnInit або з колбека кине помилку.

Ієрархія інжекторів

ROOT — ВЕСЬ ЗАСТОСУНОК providedIn: 'root' · один екземпляр на всіх КОМПОНЕНТ-БАТЬКО providers: [TaskDraftStore] — свій екземпляр КОМПОНЕНТ-ДИТИНА inject(TaskDraftStore) знайшов тут → бере цей до root уже не піде Пошук завжди йде знизу вгору й зупиняється на першому знайденому провайдері.
Провайдер на компоненті дає окремий екземпляр для цієї гілки дерева. Так роблять чернетки форм і локальні стори, які не мають переживати закриття екрана.
@Component({
  selector: 'app-task-modal',
  providers: [TaskDraftStore],   // свій екземпляр на кожну відкриту модалку
})
export class TaskModal {
  private draft = inject(TaskDraftStore);
}

Це знадобиться в модулі 6: чернетка форми має вмирати разом із модалкою, а не жити в глобальному сторі до кінця сесії.

InjectionToken: коли залежність не клас

import { InjectionToken, inject } from '@angular/core';

export interface AppConfig {
  apiUrl: string;
  pageSize: number;
}

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

// у app.config.ts
providers: [
  { provide: APP_CONFIG, useValue: { apiUrl: '/api', pageSize: 25 } },
]

// у будь-якому місці
const cfg = inject(APP_CONFIG);   // тип AppConfig, без any

Інтерфейс не існує в рантаймі (про це буде урок про strict), тому за нього не можна «зачепитись». InjectionToken — це рантайм-ключ із прив'язаним типом. Класичні випадки: конфіг, базовий URL, фіче-прапорці.

Чотири способи щось надати

ФормаЩо робитьКоли
useClassСтворити екземпляр іншого класуПідмінити реалізацію: мок замість справжнього API
useValueВіддати готове значенняКонфіг, константи, спай у тестах
useFactoryПорахувати значення функцієюКоли залежить від середовища або інших залежностей
useExistingПсевдонім наявного провайдераДва токени, один екземпляр
// підміна на моки — рівно те, що знадобиться в модулі 8
providers: [
  { provide: TaskApiService, useClass: TaskApiMock },
]

Модифікатори пошуку

const logger = inject(Logger, { optional: true });   // null, якщо немає — без помилки
const parentStore = inject(Store, { skipSelf: true });// шукати з батька, не в себе
const localOnly = inject(Store, { self: true });      // лише свій інжектор
const upToHost = inject(Store, { host: true });       // не вище хост-компонента

Використовуються рідко, але optional: true варто знати: це стандартний спосіб зробити залежність необовʼязковою, не ламаючи застосунок.

Як пояснити DI на співбесіді за 20 секунд

«Компонент не створює свої залежності сам, а просить їх у Angular через inject(). Angular тримає дерево інжекторів: шукає провайдера знизу вгору й віддає перший знайдений. Це дає один екземпляр сервісу на застосунок, дозволяє підміняти реалізації в тестах і робить компоненти незалежними від того, як саме влаштовані їхні залежності.»

Ката: як це працює всередині

Найшвидший спосіб перестати вважати DI магією — написати найпростіший інжектор самому. Він поміщається у два десятки рядків.

≈ 55 хв · +25 XP за урок