Angular - inject() function

Photo by Erik Kroon on Unsplash

Angular - inject() function

A subtle yet powerful change in dependency injection

ยท

3 min read

Probably one of the things that I love about Angular is the dependency injection capability that comes with OOTB.

I thought .NET's dependency injection usage was simple, but the one from Angular is way better from a developer experience perspective.


How we used it?

All you have to do is declare the service that you want to create, annotate it with Injectable() and that's it. Next time you will want to use it you just declare it in the constructor of the component or a service and you will automatically have access to it.

Let's have this example. We have this AmenitiesService responsible for the amenities resource from the backend. We can see that if we want to use the HttpClient we simply declare it in the constructor.

import { Injectable } from '@angular/core';
import { HttpClient } from "@angular/common/http";
@Injectable({
  providedIn: 'root'
})
export class AmenitiesService {
  constructor(readonly http: HttpClient) { }
  getAmenities() {
    this.http.get('https://my-amenities-api.com/amenities')
  }
}

Next step, we have a service that works exclusively for a hospitality provider. It inherits the functionality of AmenitiesService but with something more. Let's call it HospitalityProviderService .

import { Injectable } from '@angular/core';
import { AmenitiesService } from "./amenities.service";
import { HttpClient } from "@angular/common/http";

@Injectable({
  providedIn: 'root'
})
export class HospitalityProviderService extends AmenitiesService {
  constructor(http: HttpClient) {
    super(http)
  }

  notifyHelpdesk() {
    this.http.get('https://my-hospitality-provider-api.com/helpdesk')
  }
}

Yep, I declare guilty for using inheritance over composition in this example but let's be honest here, who hasn't used it in production? ๐Ÿ˜‰

If you've had this sort of approach in the past, you can already see the issues. The code doesn't react well to change. Especially if we need to add a new parameter to the amenities service.

Let's say we want to add a mapper service that converts from backend responses to some of the interfaces that we're using in the app.

We can already see that we need to change in multiple places if we want this change:

  • Change the AmenitiesService's constructor parameters

  • Change the HospitalityProviderService 's constructor parameters

  • Add mapper service in the super() parameters

That many changes for just one service. Imagine now having 10 service providers. And we know that production code can get very intertwined and complex.


How we will use inject() in the future?

What Angular provided together with version 14 is the inject() function that makes it easier to use dependency injection in your code.

Let's rewrite a bit the code using inject() :

@Injectable({
  providedIn: 'root'
})
export class AmenitiesService {
  http = inject(HttpClient)
  mapper = inject(MapperService)

  constructor() { }

  getAmenities() {
    this.http.get('https://my-amenities-api.com/amenities').pipe(
      map((amenities: any) => this.mapper.map(amenities))
    )
  }
}

As you can see, we removed parameters from the constructor and declared them at the class level.

@Injectable({
  providedIn: 'root'
})
export class HospitalityProviderService extends AmenitiesService {
  constructor() {
    super()
  }

  notifyHelpdesk() {
    this.http.get('https://my-hospitality-provider-api.com/helpdesk')
      .pipe(
        map((helpdesk: any) => this.mapper.map(helpdesk))
      )
  }
}

And for the HospitalityProviderService, we can see that we don't need to pass the parameters anymore in the constructor or super because dependency injection is already handled in the base class that was inherited.

Automatically, if we change something in the AmenitiesService, the changes will automatically be propagated to the services that inherit it.


Conclusion

I will use this approach in the future as it seems to be very change-proof not just for inheritance cases but for regular cases as well.

Having a parameterless constructor makes it easier for developers to use classes and don't bother with how objects are created. From my perspective, introducing inject() function feels like DI on top of DI which I don't mind at all.

For further research, I recommend taking a look over two things:

ย