Модуль 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 або з колбека кине помилку.
Ієрархія інжекторів
@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 варто знати: це стандартний спосіб
зробити залежність необовʼязковою, не ламаючи застосунок.
«Компонент не створює свої залежності сам, а просить їх у Angular через inject().
Angular тримає дерево інжекторів: шукає провайдера знизу вгору й віддає перший знайдений.
Це дає один екземпляр сервісу на застосунок, дозволяє підміняти реалізації в тестах
і робить компоненти незалежними від того, як саме влаштовані їхні залежності.»
Ката: як це працює всередині
Найшвидший спосіб перестати вважати DI магією — написати найпростіший інжектор самому. Він поміщається у два десятки рядків.