Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Ієрархія інжекторів в Angular». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Ієрархія інжекторів в Angular** - це дерево екземплярів `Injector`, що відображає дерево компонентів. При резолвінгу токена Angular рухається від найближчого інжектора вгору, поки не знайде провайдер або не кине `NullInjectorError`. ```ts @Injectable({ providedIn: 'root' }) // Один синглтон для всього застосунку export class AuthService {} @Component({ providers: [AuthService] // Новий екземпляр на компонент, перекриває root }) export class ChildComponent {} ``` **Ключове:** дочірні інжектори наслідують батьківські, але перекривають їх, якщо самі декларують той самий токен.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Ієрархія інжекторів в Angular** - це дерево екземплярів `Injector`, що відображає дерево компонентів. Коли Angular резолвить залежність, він починає з найближчого інжектора і рухається вгору, поки не знайде провайдер або не кине помилку. ## Теорія ### Коротко - Як корпоративна ієрархія: спочатку питаємо свого менеджера (локальний інжектор), потім керівника відділу (модуль), потім CEO (root) - Дочірній інжектор наслідує від батьківського, але перекриває його, якщо сам декларує провайдер для того ж токена - `providedIn: 'root'` створює один синглтон для всього застосунку; `Component.providers` створює новий екземпляр для кожного компонента - Ліниво завантажені модулі мають окремий інжектор, тому сервіс у `NgModule.providers` лінивого модуля НЕ є спільним з root - Правило вибору: спільний стан по всьому застосунку = root, ізоляція фічі = модуль, дані для конкретного екземпляра = компонент ### Швидкий приклад ```ts // Root інжектор має UserService (providedIn: 'root') @Injectable({ providedIn: 'root' }) export class UserService { name = 'real user'; } // ChildComponent декларує власний провайдер - перекриває root @Component({ selector: 'app-child', template: '...', providers: [{ provide: UserService, useClass: MockUserService }] }) export class ChildComponent { constructor(private user: UserService) { console.log(user instanceof MockUserService); // true - локальний інжектор перемагає } } ``` Дочірній компонент отримає `MockUserService`, навіть якщо в root є `UserService`. Сусідні компоненти `ChildComponent` і далі отримуватимуть root-екземпляр. ### Як Angular резолвить залежності Коли компонент або сервіс запитує токен, Angular рухається вгору по дереву інжекторів: 1. Перевіряє власний інжектор компонента (оголошений у `providers[]`) 2. Якщо не знайдено - перевіряє інжектор батьківського компонента 3. Продовжує вгору через всіх предків 4. Досягає інжектора модуля 5. Досягає root (application) інжектора 6. Якщо нічого не знайдено - кидає `NullInjectorError` Кожен екземпляр `Injector` зберігає резолвнуті сервіси в `records` map. Тому один і той самий токен не інстанціюється двічі в межах одного інжектора. Це і є гарантія синглтона на кожному рівні. ### Чотири рівні інжекторів **Root інжектор.** `@Injectable({ providedIn: 'root' })` реєструє сервіс на рівні застосунку. Один екземпляр на весь застосунок, створюється лениво при першому запиті. Підтримує tree-shaking: якщо сервіс ніхто не інжектує, він не потрапляє в бандл. **Інжектор модуля.** Якщо сервіс вказаний у `@NgModule.providers`, він живе в інжекторі цього модуля. Для eagerly loaded модулів це зливається з root. Для lazy-loaded модулів створюється дочірній інжектор, тобто сервіс відокремлений від root. **Інжектор компонента.** Додаючи сервіс у `@Component.providers`, отримуємо новий екземпляр для кожного екземпляра компонента. Цей екземпляр знищується разом із компонентом. Дочірні компоненти наслідують його, якщо не перевизначають. **Інжектор елемента (директиви).** `@Directive.providers` працює так само, як провайдери компонента, але прив'язаний до елемента, де застосовується директива. ### Коли що використовувати - Спільний стан по всьому застосунку (авторизація, логування, feature flags): `providedIn: 'root'` - Ізоляція фічі в lazy-loaded роуті: `NgModule.providers` всередині лінивого модуля - Дані для конкретного екземпляра компонента (локальний стан форми, кеш картки): `Component.providers` - Мокування сервісу в піддереві для тестів або e2e: провайдер на рівні компонента або модуля з `useClass` - Бібліотечні сервіси, що не повинні забруднювати глобальний простір: `providedIn: 'platform'` або рівень модуля ### Як Angular будує це під капотом При завантаженні Angular рекурсивно створює екземпляри `Injector` для кожного `@Component` з масивом `providers[]` і пов'язує кожен із батьківським через властивість `parent`. Під час резолвінгу викликається `getAt()`, що рухається вгору від місця запиту. Результати кешуються в `records` map кожного інжектора, тому повторний запит того ж токена повертає закешований екземпляр без повторної інстанціації. В Angular 14+ `providedIn: 'root'` аналізується статично, що уможливлює tree-shaking. Сервіси в масивах `providers[]` не підлягають tree-shaking, бо вони явно вказані в метаданих модуля або компонента. Standalone-компоненти (Angular 14+) частково спрощують цю картину: вони використовують `EnvironmentInjector` замість `NgModule`-інжекторів. Але провайдери на рівні компонента і далі створюють елементні інжектори так само. ### Типові помилки **Помилка 1: очікування що провайдер лінивого модуля використовує root-екземпляр** ```ts // Неправильно: це створює ОКРЕМИЙ екземпляр, а не root-синглтон @NgModule({ providers: [DataService] // Ліниво завантажений модуль має власний інжектор! }) export class FeatureModule {} ``` Якщо `DataService` також `providedIn: 'root'`, провайдер лінивого модуля перекриє його для всього, що всередині. Компоненти поза модулем і далі отримують root-екземпляр. Щоб використовувати root: видали з `NgModule.providers` і покладайся лише на `providedIn: 'root'`. **Помилка 2: оголошення сервісу і в модулі, і в компоненті** ```ts @NgModule({ providers: [CartService] }) // ...і також: @Component({ providers: [CartService] }) export class CartComponent {} ``` Інжектор компонента перекриє інжектор модуля. Ти отримаєш два окремі екземпляри, що витрачають зайву пам'ять. Angular DevTools покаже це: відкрий дерево компонентів і перевір джерела інжектора для кожного компонента. **Помилка 3: забуваєш що дочірні компоненти наслідують провайдери батьківського компонента** ```ts @Component({ providers: [SharedService] }) export class ParentComponent {} // ChildComponent отримає екземпляр SharedService від ParentComponent // НЕ root-екземпляр, НЕ новий @Component({ selector: 'app-child', template: '' }) export class ChildComponent { constructor(private s: SharedService) {} } ``` Я бачив як команди додають провайдер до батьківського компонента "тільки для цього батька", а потім дивуються чому дочірній компонент отримує несподіваний стан. Дочірній наслідує автоматично. **Помилка 4: `providedIn: 'root'` у бібліотеці, що публікується** Це реєструє сервіс у root-інжекторі застосунку під час збірки. Для звичайних застосунків - нормально. Але для бібліотек це може конфліктувати зі споживачами. Використовуй `providedIn: 'platform'` для сервісів, що мають бути спільними між мікрофронтендами, або залиш вибір застосунку-споживачу. **Помилка 5: плутанина між `viewProviders` і `providers`** `viewProviders` обмежує сервіс лише view компонента (шаблоном), виключаючи контент, проєктований через `<ng-content>`. `providers` ділиться і з view, і з content-дочірніми. Неправильний вибір призводить до `NullInjectorError` у проєктованих компонентах, що намагаються інжектувати цей сервіс. ### Де зустрічається в реальних проєктах - Angular Material: `OverlayRef` і сервіси діалогів використовують `providedIn: 'root'` для синглтона overlay-контейнера - NgRx: `StoreModule.forRoot()` реєструє стан на рівні root; `StoreModule.forFeature()` додає стан фічі в дочірній інжектор модуля - Nx workspaces: ліниві feature-бібліотеки використовують провайдери на рівні модуля для ізоляції сервісів від shell-застосунку - Angular Universal (SSR): провайдери на рівні платформи замінюють браузерні API на Node-сумісні без будь-яких змін у root ### Питання на співбесіді **Q:** Що відбувається коли lazy-loaded модуль надає сервіс, що також є `providedIn: 'root'`? **A:** Інжектор лінивого модуля перекриє root для будь-якого компонента всередині цього модуля. Компоненти поза лінивим модулем і далі отримують root-синглтон. Два окремі екземпляри існують одночасно в одному застосунку. **Q:** Як `@SkipSelf()` змінює резолвінг? **A:** Він пропускає інжектор поточного компонента і починає пошук з батьківського. Корисно коли сервіс потребує доступу до своєї батьківської версії, наприклад вузол дерева, якому потрібен `TreeService` батька, а не свій власний. **Q:** Що робить `@Self()`? **A:** Обмежує пошук лише інжектором поточного компонента. Якщо токен там не знайдено - Angular одразу кидає помилку без підйому по дереву. Використовується щоб гарантувати що залежність обов'язково надається на тому ж рівні, що й споживач. **Q:** Як standalone-компоненти змінюють ієрархію інжекторів? **A:** Standalone-компоненти (Angular 14+) використовують `EnvironmentInjector` замість `NgModule`-інжекторів. Функція `bootstrapApplication()` створює root `EnvironmentInjector`. Провайдери на рівні компонента і далі створюють елементні інжектори. Функція `inject()` в реактивних контекстах, зокрема в сигналах, використовує те саме дерево. **Q (senior):** Директива застосована до компонента і директива інжектує сервіс. Де Angular шукає цей сервіс? **A:** Пошук починається в інжекторі елемента, на якому стоїть директива. Він включає провайдери і компонента, і директиви одночасно. Після цього Angular рухається вгору по дереву компонентів звичайним чином. Директива не має окремої гілки інжектора; вона поділяє контекст інжектора елемента з хост-компонентом. ## Приклади ### Базовий: ізольований екземпляр на компонент ```ts // counter.service.ts @Injectable() export class CounterService { count = 0; increment() { this.count++; } } // card.component.ts @Component({ selector: 'app-card', template: ` <button (click)="counter.increment()">+</button> <span>{{ counter.count }}</span> `, providers: [CounterService] // Кожна картка отримує свій власний лічильник }) export class CardComponent { constructor(public counter: CounterService) {} } ``` Два елементи `<app-card>` на одній сторінці показують незалежні лічильники. Якби `CounterService` був `providedIn: 'root'`, вони б ділили стан і обидві кнопки збільшували б одне число. ### Середній: мокування сервісу в lazy-loaded фічі ```ts // auth.service.ts @Injectable({ providedIn: 'root' }) export class AuthService { isLoggedIn = false; login() { this.isLoggedIn = true; } } // mock-auth.service.ts @Injectable() export class MockAuthService extends AuthService { isLoggedIn = true; // Завжди залогований для e2e-тест модуля } // auth-test.module.ts (lazy loaded) @NgModule({ providers: [{ provide: AuthService, useClass: MockAuthService }] }) export class AuthTestModule {} // dashboard.component.ts (всередині AuthTestModule) @Component({ template: `<span *ngIf="auth.isLoggedIn">Вітаємо</span>` }) export class DashboardComponent { constructor(public auth: AuthService) {} // Отримує MockAuthService - завжди показує "Вітаємо" } ``` Root-екземпляр `AuthService` залишається незміненим. Лише компоненти всередині `AuthTestModule` бачать мок. Все поза лінивою межею і далі використовує реальний синглтон. ### Просунутий: `@SkipSelf()` для самореференційних сервісів ```ts // logger.service.ts @Injectable() export class LoggerService { constructor( @Optional() @SkipSelf() private parent: LoggerService ) {} log(msg: string) { const prefix = this.parent ? '[child]' : '[root]'; console.log(`${prefix} ${msg}`); } } // parent.component.ts @Component({ selector: 'app-parent', template: '<app-child></app-child>', providers: [LoggerService] // Root логер }) export class ParentComponent { constructor(private logger: LoggerService) { this.logger.log('parent init'); // "[root] parent init" } } // child.component.ts @Component({ selector: 'app-child', template: '', providers: [LoggerService] // Дочірній логер, знає про батька }) export class ChildComponent { constructor(private logger: LoggerService) { this.logger.log('child init'); // "[child] child init" } } ``` `@SkipSelf()` каже Angular шукати `LoggerService` починаючи з батьківського компонента, а не з поточного. У поєднанні з `@Optional()` це уникає помилки кругової залежності на рівні root, де батьківського логера просто немає.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.