Dependency injection in Angular

In this article, I will explain one major feature – dependency injection in Angular. In general, the DI principle states that high-level modules should not depend on low-level modules. Both of them should depend on abstraction. The DI mechanism in Angular helps us to provide an object instance wherever needed.

What is Dependency injection in Angular?

When a component, for instance, needs a service, to get some data from a backend, we may say that this component has a dependency on the service. And the DI mechanism can help us to provide a service instance to that component.

Let’s assume you are building an event system application to provide the latest deals in a certain location. Your home page will display the upcoming events. Here are the event model and the service:

TypeScript
export class EventModel {
    constructor(title: string, date: Date) {
        this.title = title;
        this.date = date;
    }

    title: string;
    date: Date;
}

export class EventService {
    getEvents(): EventModel[] {
        let events: EventModel[] = [
            new EventModel("Musical", new Date("12.12.2022")),
            new EventModel("Football game", new Date("14.12.2022")),
            new EventModel("Stand Up Comedy", new Date("27.12.2022"))
        ];

        return events;
    }
}

The wrong way of using services

In our AppComponent, we will need a dependency on the EventService, to get the events. The easiest (and wrong!) way is to hardcode the instance, like this:

TypeScript
export class AppComponent {
  events: EventModel[] = [];
  
  constructor() {
    let eventService = new EventService();
    this.events = eventService.getEvents();
  }
}

Why this is the wrong aproach:

  • The AppComponent is coupled with the eventService. You don’t have an easy way to change the service in the future.
  • The AppComponent is harder to test. When testing you will have to provide a mock to the dependent service. This is currently not possible, with this instance creation in the constructor. If the instance was somehow passed to the AppComponent from outside, it would be easier to pass a DummyEventService and execute your tests.

The right way to use services

As we saw, the problems came from the fact, that we are creating a specific service instance in our component. But, how to fix that? The answer is simple – just move the instance creation outside of the component, and pass it to the constructor:

TypeScript
export class AppComponent {
  events: EventModel[] = [];
  
  constructor(private eventService: EventService) {
    this.events = this.eventService.getEvents();
  }
}

In this way our component just needs something which is EventService, it can be anything, which is EventService. For instance – DummyEventService, for testing. The creation of the service instance is decoupled from the AppComponent. The component just asks for such a service, and through the dependency injection in angular, the service instance is provided. Let’s see which are the core elements of the DI mechanism, and how they work together.

How dependency injection in Angular works?

First of all let’s see what are the main elements in the DI mechanism:

Dependency injection in Angular - core elements
Dependency injection in Angular – core elements

And how these all elements work together:

  • The Provider keeps the information about all the dependencies. Each dependency is identified with a DI token. This is the unique ID of the dependency.
  • When the Consumer (e.g. the AppComponent) is instantiated along with its dependencies in the constructor, the Injector reads them.
  • Next, the Injector looks in the Provider, if the consumers’ dependencies are registered.
  • The Provider passes an instance to the Injector.
  • Lastly, the Injector injects the instance into the Consumer.

Let’s rewrite the previous events example to use this pattern:

TypeScript
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [EventService]
})
export class AppComponent {
  events: EventModel[] = [];

  constructor(private eventService: EventService) {
    this.events = this.eventService.getEvents();
  }
}

The providers array with the registered dependencies is defined in the @Component decorator. When a component is instantiated in Angular, it automatically gets an Injector instance. The injector reads the required components’ dependencies from its constructor and looks in the providers array for them. In our case, it will find the EventService and will inject an instance into the AppComponent.

A better approach is to register the providers on the module level, instead in the @Component decorator. Do it like this:

TypeScript
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [EventService],
  bootstrap: [AppComponent]
})
export class AppModule { }

export class AppComponent {
  events: EventModel[] = [];

  constructor(private eventService: EventService) {
    this.events = this.eventService.getEvents();
    console.log(this.events);
  }
}

In this way, the EventService will be available for each component in this module and can be reused.

Dependency injection of a service into another service

Imagine our EventService needs additional service to do its job. For instance – TicketService, which will return available tickets along with their prices. Then you have to inject the TicketService into the EventsService. First, create and register the TicketService into the providers array of the module:

TypeScript
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [EventService, TicketService],
  bootstrap: [AppComponent]
})
export class AppModule { }

export class TicketService {
    getTickets(id: number): TicketModel[] {
        // use the id to get the tickets from the backend
        // in this case they are hard coded for simplicity

        let tickets: TicketModel[] = [
            new TicketModel("VIP", 100.00),
            new TicketModel("Regular", 40)
        ];

        return tickets;
    }
}

The next step is to tell our EventService, that it needs a dependency. This is done, similarly as we do it with the AppComponent – add the required dependencies in the constructor.

But there is another additional step. We need to mark our EventService with the @Injectable decorator. This will explicitly tell Angular that this class needs some dependencies.

The @Injectable decorator wasn’t required for the AppComponents’ dependencies because the @Component decorator is a subtype of @Injectable. So, it comes out of the box for every Angular component. And the final EventService is:

TypeScript
@Injectable()
export class EventService {

    constructor(private ticketService: TicketService) { }

    getEventInfo(id: number) {
        let tickets = this.ticketService.getTickets(id);

        // ... gather another info and return it ...
    }

    getEvents(): EventModel[] {
        let events: EventModel[] = [
            new EventModel("Musical", new Date("12.12.2022")),
            new EventModel("Football game", new Date("14.12.2022")),
            new EventModel("Stand Up Comedy", new Date("27.12.2022"))
        ];

        return events;
    }
}