Funkcja inject() została wprowadzona w Angular 14 jako alternatywa dla deklarowania zależności przez właściwość providers oraz przez przekazywanie ich do konstruktora. Obecnie powszechnie preferuje się korzystanie z inject() zamiast konstruktora. W tym artykule odkryjemy zalety, które przemawiają za używaniem inject(), i wyjaśnimy, dlaczego warto go stosować.
Przyjrzymy się temu, jak korzystanie z inject() wypada w porównaniu do konstruktora w różnych scenariuszach. Ale zanim do tego przejdziemy, omówimy krótko mechanizm dependency injection, kontekst wstrzykiwania w Angularze oraz kilka zasad dotyczących samego injection.
Podsumowanie: Dependency injection (DI)
Dependency injection (DI), czyli wstrzykiwanie zależności, to powszechnie używany wzorzec projektowy, który implementuje zasadę Inversion of Control. Angular posiada potężny wbudowany mechanizm DI, który umożliwia tworzenie i dostarczanie części aplikacji do innych elementów, które ich potrzebują. Dzięki temu możesz korzystać z zależności w aplikacji w elastyczny sposób.
W systemie DI mamy:
- konsumenta zależności – klasę lub komponent, który potrzebuje jakiejś zależności,
- provider zależności – miejsce, które udostępnia tę zależność.
Providerami mogą być różne elementy, najczęściej jest to klasa oznaczona dekoratorem @Injectable z ustawionym providedIn. Przykład:
@Injectable({
providedIn: 'root'
})
class HeroService {}
Ustawienie root sprawia, że klasa jest singletonem i zostaje zarejestrowana w root injectorze aplikacji. Można to też zrobić tak:
@Injectable()
class HeroService {}
// ...
@Component({
selector: 'app-example',
template: '...',
providers: [HeroService]
})
export class ExampleComponent {
private _heroService = inject(HeroService);
}
Tutaj powstaje instancja o zasięgu lokalnym zamiast singletona – trzeba ją ręcznie dodać do tablicy providers komponentu, w którym chcemy jej używać.
W praktyce zależnościami są najczęściej serwisy i własne klasy z @Injectable.
Nad tym wszystkim czuwa abstrakcja Injector, która przy każdym żądaniu sprawdza, czy dana instancja już istnieje, a jeśli nie – tworzy nową i rejestruje.
!ZDJĘCIE!
Injectory są tworzone podczas procesu bootstrapowania aplikacji, więc nie trzeba ich tworzyć ręcznie. Więcej informacji o dependency injection mozna znalezc tu
Injection context
Dependency injection opiera się na kontekście wykonywania w czasie działania aplikacji, zwanym injection context. Gdy konsument zależności chce wstrzyknąć serwisy, dyrektywy, pipes czy inne klasy oznaczone jako @Injectable, może to zrobić tylko w ramach injection context. Zasady DI mówią jasno: każda forma wstrzykiwania musi być użyta w tym kontekście – w przeciwnym razie wystąpi błąd.
Przykład:
class MyComponent {
private _service1: Service1;
private _service2: Service2 = inject(Service2); // In context
private _service3: Service3;
constructor(private _service4: Service4) { // In context
this._service1 = inject(Service1) // In context
}
data = getData(); // In context
onSubmit() {
this._service3 = inject(Service3) // Out of context
this._service1.method() // Still allowed
}
}
export function getData(): HttpClient {
return inject(HttpClient);
}
Przypadki, w których injection context jest dostępny:
- blok konstrukcyjny klasy oznaczonej @Injectable lub @Component, w tym constructor() i pola inicjalizacyjne tych klas,
- funkcja określona w useFactory providera lub w @Injectable,
- funkcja w InjectionToken,
- ramka stosu, która działa w kontekście wstrzykiwania
Wiecej informacji o kontekscie wstrzykiwania mozesz znalezc tu
Jeżeli spróbujesz wstrzyknąć serwis poza kontekstem, Angular rzuci błąd NG0203.
Sprawdzmy ponizszy przyklad:
import { Component, OnInit, inject, signal } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app.config';
import { TodoService } from './todo.service';
import { User, UserService } from './user.service';
import 'zone.js';
@Component({
selector: 'app-root',
standalone: true,
template: `
<h2>ToDos</h2>
<select (change)="onSelected($event)">
<option value="">--Select a user--</option>
@for(user of users(); track user.id) {
<option [value]=user.id>{{ user.name }}</option>
}
</select>
<button (click)='onClick()'>New Item</button>
@for(todo of todosForUser(); track todo.id) {
<div>* {{ todo.title }}</div>
}
`,
})
export class App implements OnInit {
private readonly _todoService = inject(TodoService);
protected todosForUser = this.todoService.todosForDisplay;
// private readonly _userService = inject(UserService);
// users = this._userService.users;
users = signal<User[]>([]).asReadonly();
// constructor(private _userService: UserService,
// private _todoService: TodoService) {}
ngOnInit() {
const userService = inject(UserService);
this.users = userService.users;
}
onClick() {
this._todoService.addNewItem();
}
onSelected(event: Event) {
const selectedVal = (event.target as HTMLSelectElement).value;
if (typeof selectedVal !== 'string') return;
this._todoService.getTodosForUser(selectedVal);
}
}
bootstrapApplication(App, appConfig);
Powyższy przykład wstrzykuje userService wewnątrz hooka cyklu życia OnInit w celu inicjalizacji. Nie spowoduje to błędu w kodzie, ale logika zawiedzie, ponieważ wstrzykiwanie działa w fazie konstrukcji (czyli w ramach injection context), a nie po jej zakończeniu. Dodatkowo w konsoli zobaczysz następujący błąd:
!ZDJĘCIE!
W przypadku Twojej aplikacji, ponieważ logika zawodzi, nie zobaczysz żadnych użytkowników na liście:
!ZDJĘCIE!
Powyższy przykład pochodzi ze StackBlitz, gdzie możesz go znaleźć pod tym linkiem i samemu się nim pobawić 🙂
Jak inject() wypada na tle konstruktora?
Wstrzykiwanie zależności przez konstruktor to tradycyjna metoda i nadal jest wspierana w najnowszych wersjach Angulara. W Angularze 14 pojawiła się jednak funkcja inject(), która pobiera token z aktywnego Injectora. Tak jak inne metody DI, działa ona tylko w ramach injection context.
Technicznie konstruktor nie może „wypaść” z kontekstu injection (w przeciwieństwie do inject()), ale inject() daje znacznie więcej możliwości i często upraszcza kod. Dlatego dziś jest metodą bardziej popularną niż konstruktor.
Wstrzykiwanie do standalone functions
Standalone functions to zewnętrzne funkcje, których można używać wewnątrz klasy.
W metodzie z konstruktorem wygląda to tak:
export class App implements OnInit {
// ...
constructor(private _userService: UserService, private _todoService: TodoService) {}
ngOnInit() {/* ... */}
// ...
}
Sposób pokazany powyżej to skrócona forma wstrzykiwania zależności podczas fazy konstrukcji. Korzystając z konstruktora jako metody injection, jesteś ograniczony wyłącznie do fazy konstrukcyjnej klasy, ponieważ funkcje nie mają konstruktorów. To prowadzi do wniosku, że wstrzykiwanie wewnątrz standalone functions jest możliwe. Jeśli wywołasz funkcję, która wstrzykuje serwis, to czy znajdzie się ona w injection context, zależy od miejsca, w którym została wywołana. Spójrzmy na ten przykład:
import { HttpClient } from "@angular/common/http";
import { Injectable, inject } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
export interface User {
id: string;
name: string;
username: string;
email: string;
website: string;
}
export function getUsers() {
const userUrl = "https://jsonplaceholder.typicode.com/users";
const http = inject(HttpClient);
return toSignal(http.get<User[]>(userUrl), {initialValue:[] });
}
@Injectable({
providedIn: 'root'
})
export class UserService {
// private _userUrl = "https://jsonplaceholder.typicode.com/users";
// private readonly _http = inject(HttpClient); -> injection context
// constructor(private _http: HttpClient) { } -> injection context
// users = toSignal(this._http.get<User[]>(this._userUrl), {initialValue:[] });
// injection context
users = getUsers();
}
Jak widać, funkcja getUsers() pełni rolę w inicjalizacji zmiennych w fazie konstrukcji klasy UserService. Dlatego wywołanie inject(HttpClient) również odbywa się w injection context. Jednak ta metoda wiąże cię z używaniem inject(), ponieważ wstrzykiwanie oparte na konstruktorze nie jest możliwe. Dodatkowo, możliwość wywoływania inject() w różnych miejscach wewnątrz bloku konstrukcji klasy daje też szansę na wykonywanie niektórych zadań konstrukcyjnych z zewnątrz klasy poprzez eksportowane funkcje.
Dziedziczenie
Kolejnym przypadkiem, w którym funkcja inject() upraszcza DI, jest praca z dziedziczeniem. Wracamy do klas, więc teraz wyjaśnię, o co nie musisz się martwić dzięki inject(). Weźmy za przykład klasę BaseComponent oraz klasę ChildComponent, która dziedziczy po BaseComponent:
// base.component.ts
import { OnInit } from '@angular/core';
import { LoggerService } from './logger.service';
import { ErrorHandlerService } from './error-handler.service';
export abstract class BaseComponent implements OnInit {
constructor(
protected logger: LoggerService,
protected errorHandler: ErrorHandlerService
) {}
ngOnInit() {
this.logger.log('BaseComponent Initialized');
}
}
Klasa BaseComponent wstrzykuje LoggerService i ErrorHandlerService poprzez swój konstruktor.
// child.component.ts
import { Component } from '@angular/core';
import { BaseComponent } from './base.component';
import { LoggerService } from './logger.service';
import { ErrorHandlerService } from './error-handler.service';
import { AnalyticsService } from './analytics.service';
@Component({
selector: 'app-child',
template: `...`
})
export class ChildComponent extends BaseComponent {
constructor(
protected override logger: LoggerService,
protected override errorHandler: ErrorHandlerService,
private analytics: AnalyticsService
) {
super(logger, errorHandler);
}
trackEvent() {
this.analytics.track('Child Event');
this.logger.log('Event tracked from ChildComponent');
}
}
Klasa ChildComponent wstrzykuje dla siebie klasę AnalyticsService. Zauważ jednak, że jej konstruktor przyjmuje również serwisy, które wstrzykuje BaseComponent, aby móc je przekazać wywołaniem super() w odpowiedniej kolejności do konstruktora bazowego. Ten sposób może być uciążliwy do wdrożenia, szczególnie w większych projektach.
Teraz przejdźmy do wersji, w której zastępujemy to funkcją inject():
// base.component.ts
import { OnInit, inject } from '@angular/core';
import { LoggerService } from './logger.service';
import { ErrorHandlerService } from './error-handler.service';
export abstract class BaseComponent implements OnInit {
protected logger = inject(LoggerService);
protected errorHandler = inject(ErrorHandlerService);
constructor() {}
ngOnInit() {
this.logger.log('BaseComponent Initialized');
}
}
Konstruktor klasy BaseComponent nie przyjmuje żadnych argumentów. W tym przypadku blok constructor() można nawet pominąć, aby kod był czytelniejszy.
// child.component.ts
import { Component, inject } from '@angular/core';
import { BaseComponent } from './base.component';
import { AnalyticsService } from './analytics.service';
@Component({
selector: 'app-child',
template: `...`
})
export class ChildComponent extends BaseComponent {
private analytics = inject(AnalyticsService);
constructor() {
super();
}
trackEvent() {
this.analytics.track('Child Event');
this.logger.log('Event tracked from ChildComponent');
}
}
ChildComponent wstrzykuje to, czego potrzebuje. Ponieważ już dziedziczy po klasie BaseComponent, w kwestii konstruktora pozostaje znacznie mniej pracy. Nie musisz martwić się o kolejność propertiesów – wystarczy wywołać pusty super() (JavaScript tego wymaga, ale cóż 😄).
Wstrzykiwanie warunkowe
Wstrzykiwanie warunkowe to kolejny przypadek, w którym jesteśmy związani z funkcją inject(), ponieważ możemy wstrzyknąć zależność w oparciu o warunek, jak sama nazwa wskazuje. Nie można wywołać constructor() wewnątrz bloku if, więc zamiast tego w konstruktorze sprawdzasz warunek i wywołujesz funkcję inject(), co jest prawidlowe w kontekście wstrzykiwania. Zobaczmy ten przykład:
import { inject, Injectable, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { AnimationService } from './animation.service';
@Injectable({ providedIn: 'root' })
export class UiOrchestrationService {
private readonly _animationService = isPlatformBrowser(inject(PLATFORM_ID))
? inject(AnimationService)
: null;
// constructor() {
// const platformId = inject(PLATFORM_ID);
// if (isPlatformBrowser(platformId)) {
// this.animationService = inject(AnimationService);
// }
// }
triggerAnimation() {
this.animationService?.start();
}
}
W powyższym przykładzie mamy AnimationService, który działa tylko w środowisku przeglądarki. Najpierw wstrzykuje PLATFORM_ID, który jest specjalnym tokenem informującym, na jakiej platformie działa Twoja aplikacja. Tworzysz instancję AnimationService jako null, następnie w konstruktorze sprawdzasz warunek i dopiero wtedy wstrzykujesz serwis. Dzięki takiemu podejściu możesz uniknąć błędów w czasie wykonywania po stronie serwera oraz poprawić wydajność, nie tworząc niepotrzebnych serwisów.
Inne sposoby użycia inject()
Do tej pory poznałeś koncepcję DI, zasady kontekstu wstrzykiwania oraz różnicę między funkcją inject() a wstrzykiwaniem przez konstruktor. Nauczyłeś się, gdzie używać wstrzykiwania, a gdzie nie, oraz jak rygorystyczne są zasady kontekstu wstrzykiwania.
Mimo że jest to temat bardziej zaawansowany, chciałabym przedstawić inne sposoby dostępne w Angularze, które pozwalają używać inject() tam, gdzie mogłoby się wydawać to niemożliwe. Obejmuje to kilka funkcji pomocniczych.
runInInjectionContext:
Ta funkcja pomocnicza umożliwia wywołanie inject() w kontekście wstrzykiwania, nawet jeśli faktycznie nie znajdujesz się w nim (np. w metodach czy hookach cyklu życia).
// hero.service.ts
@Injectable({
providedIn: 'root',
})
export class HeroService {
private _environmentInjector = inject(EnvironmentInjector);
someMethod() {
runInInjectionContext(this._environmentInjector, () => {
inject(SomeService); // Do what you need with the injected service
});
}
}
przyklad z https://angular.dev/guide/di/dependency-injection-context#run-within-an-injection-context
Korzystając z tej metody, musisz również mieć dostęp do bieżącego injektora.
injector.runInContext()
Ta metoda jest używana w ten sam sposób, co wcześniej: runInInjectionContext jest funkcją samodzielną, natomiast runInContext jest metodą obiektu EnvironmentInjector.
import { inject, Injectable, EnvironmentInjector } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class DataService {
private _injector = inject(EnvironmentInjector);
loadDataAsynchronously() {
setTimeout(() => {
this._injector.runInContext(() => {
const httpClient = inject(HttpClient);
// ...
});
}, 2000);
}
}
Podsumowanie
Konwencjonalnie, metody wstrzykiwania zależności (DI) są ściśle związane z kontekstem wstrzykiwania, co oznacza, że nie można ich używać poza tym kontekstem. W tym artykule przyjrzeliśmy się różnicom między wstrzykiwaniem zależności opartym na konstruktorze a wstrzykiwaniem funkcjonalnym. Wstrzykiwanie zależności przez konstruktor jest tradycyjną metodą i nie może być wywoływane w miejscach znajdujących się poza kontekstem wstrzykiwania, dlatego naszym głównym narzędziem jest funkcja inject().
Dowiedziałeś się, gdzie można używać funkcji inject(), a gdzie nie, oraz jak korzystać z niej poza kontekstem wstrzykiwania, jednocześnie sprawiając, że znajduje się on w kontekście. Poznałeś także, jak bardzo inject() upraszcza dziedziczenie, umożliwia warunkowe wstrzykiwanie zależności i pozwala na wstrzykiwanie wewnątrz funkcji samodzielnych.
Wstrzykiwanie zależności oparte na funkcjach jest obecnie najpopularniejszym sposobem DI, co jest niekwestionowane, biorąc pod uwagę korzyści i elastyczność, jaką przynosi zarówno aplikacjom Angular, jak i developer experience
Related article: https://angular.love/dependency-injection-in-angular-everything-you-need-to-know