Back to the homepage
Angular

Dependency Injection in Angular – everything you need to know

Dependency Injection (DI) is one of the most important mechanisms in Angular. This pattern allows for inversion of control by passing instances of requested dependencies to the class instead of creating them inside the class. This approach creates loose coupling and makes testing easier.

In this article I would like us to take a closer look at how it works. We will find out how dependencies are defined, created and resolved and how developers can customize this process. I invite you to explore the ins and outs of Dependency Injection in Angular and discover why DI is a key concept in designing applications in Angular, the benefits of using it, and how to apply it effectively in practice.

This article is inspired by Angular Dependency Injection series on YouTube channel Decoded Frontend. If you’re looking for advanced Angular tutorials this is a great place to visit.

How to inject dependencies in Angular?

Angular allows the injection of necessary dependencies, such as classes, functions, or primitives to classes decorated with @Component, @Directive, @Pipe, @Injectable and @NgModule by defining them as a constructor parameter:

or using inject function:

The inject function in its current form was introduced in version 14. Aside from bringing a convenient and readable way of declaring dependencies, it also offers the following advantages:

  • It allows the omission of explicit typing — let TypeScript do it for you.
  • Class extension is made easier without the necessity of passing every argument to the base class constructor.
  • Additionally, it lets the programmer move the logic to reusable functions — the downside here, however, is hiding dependencies inside the function.

It’s worth remembering that the inject function can only be used inside an injection context. This means 

  • Within a constructor,
  • As a definition of a class field,
  • Inside the factory function,
  1. as useFactory in the Provider interface,
  2. @Injectable decorator or a factory in the Injection token definition,
  • An API within the injection context, such as a router guard or a runInInjectionContext function callback.

How does the Angular Injector work?

An abstraction called Injector is responsible for resolving dependencies. It can store an instance of a required dependency. If it already exists, it’s passed onto the consumer. Otherwise, a new instance is created and passed as a constructor parameter and stored in memory. Every dependency inside an Injector is a singleton — which means there’s always only one instance.

To better demonstrate this process, let’s create a simple example. Let’s assume that we have a class representing some service:

and a class representing a component which uses the service:

The injector is responsible for storing and returning an instance of the dependency:

During the bootstrapping, Angular creates the Injector and registers dependencies which will be passed to components:

Hierarchical Injectors in Angular

Dependencies can be defined on several levels and organized in hierarchies. This is done using different types of injectors:

  • Element Injector — registers dependencies defined in providers inside the @Component or @Directive decorators. These dependencies are available for the component and its children.

  • Environment Injector — child hierarchies of the environment injector are created whenever dynamically loaded components are created, such as with a router. In that case, the injector is available for components and its children. It is higher in the hierarchy than the element injector in the same component.

Environment Root Injector — contains globally available dependencies decorated with @Injectable and having providedIn set to “root” or “platform”.

or defined in providers of the ApplicationConfig interface:

To achieve better optimization, it’s recommended to use the @Injectable decorator. Such a definition makes dependencies tree-shakeable — they are removed from bundled files if they haven’t been used.

  • Module Injector — in module-based applications, this injector stores global dependencies decorated with @Injectable and having providedIn set to “root” or “platform”. Additionally, it keeps track of dependencies defined in the providers array within @NgModule. During compilation, Angular also recursively registers dependencies from eagerly loaded modules. Child hierarchies of Module Injector are created by lazy loaded modules.
  • Platform Injector — configured by Angular, this injector registers platform-specific dependencies such as DomSanitizer or the PLATFORM_ID token. Additional dependencies can be defined by passing them to the extraProviders array in the platformBrowserDynamic function parameter.
  • Null Injector — the highest injector in the hierarchy. Its  job is to throw the error “NullInjectorError: No provider for …” unless the @Optional modifier was used.

If a component requires a dependency, Angular first looks for it in the element injector of the component. If it isn’t defined in the providers array, then the framework looks at the parent component. This process repeats for as long as Angular finds a dependency in an ancestor. If the dependency isn’t found, the next phase is searching in the environment injector (or the module injector in the case of module-based applications), and then the environment root injector. Finally, the platform injector is checked. If Angular reaches the null injector an error is thrown.

In this hierarchical order, if a dependency exists in more than injector, the instance defined on the lowest level, the one closest to the component is resolved.

Angular Resolution Modifiers

The process we just described is affected by the following modifiers:

  • @Optional makes a dependency, well, optional. If one hasn’t been resolved, instead of throwing an error Angular returns null.
  • @Self  makes Angular look for the dependency only in the element injector of the component, meaning it has to be defined in the providers array of the component. In any other case the “NodeInjector: NOT_FOUND” error is thrown.
  • @SkipSelf is the opposite of the @Self modifier. Angular starts looking for dependency from the Element Injector of the parent component.

@Host restricts the dependency lookup to the host of the element. Let’s use a simple example to demonstrate how the @Host modifier works.. Let’s assume we have a MyComponent component which uses two directives in its view: ParentDirective and ChildDirective. ChildDirective requires injecting MyService service. A built view of this fragment would look something like this: 

The host of MyComponent is restricted by its <app-my-component> tag. In practice, it means that Angular only checks the following in its search for the provider:

  • the providers array of ChildDirective,
  • the providers array of ParentDirective,
  • the viewProviders array of MyComponent.

Only components can define viewProviders. Dependencies provided there are available in the host of the component — they are not available for content projected via ng-content despite it being a logical descendant of the component.

Described decorators can be used for dependencies defined as constructor parameters. When we use the inject function, flags with names corresponding to decorators should be set in the options object:

What Is a Dependency Provider?

At this point it is worth describing more broadly what a dependency provider is. In a nutshell, it’s a recipe which tells Angular how to create an instance of dependency. 

The default and the most straightforward way is to define a TypeProvider, using class as a token. An instance of that class is created using the new operator. This is, in fact, syntactic sugar which is expanded into a full definition described by the Provider interface. Such a declaration consists of a token to identify the dependency and a definition of how an instance should be created.

Class Provider

The class provider contains the option useClass — its job is to create and resolve a new instance of the defined class. It replaces the class defined as a token by its extension, classes with different implementation, or its mock for testing purposes.

This example shows how to change implementation of dependency without making changes in the component itself.

Alias Provider

The alias provider maps one token to another, as  defined in the useExisting field. The first token is an alias for the class associated with the second one. Angular doesn’t create a new instance but instead resolves an existing one. 

This type of definition ensures that if the component depends on the Logger or TimeLogger classes, the existing instance of TimeLogger is always resolved. It’s worth noting the difference between useExisting and useClass. If we were to use useClass a new, independent instance of TimeLogger would be created.

Factory Provider

Using the factory provider, let’s create a dependency based on runtime values by calling a function defined in useFactory.

This provider contains an additional field, deps, which is an array of tokens passed as arguments of the factory function. The order in which they are defined is important. For functions with more arguments, it may be more convenient and flexible to replace them with a single Injector, which allows Angular to retrieve the needed dependencies inside the function. It would look something like this: 

Another interesting use case is when we don’t know in advance what dependency we want to use, and it is determined by some runtime condition. To use a simple example, we could have a service that connects to an external API, and we want to limit the number of requests sent so as not to generate additional costs:

Value Provider 

The value provider allows us to associate a static value defined within the useValue with a token. This technique is usually used for resolving configuration constants or mocking data in tests.

Why Do We Need an Injection Token?

In the case of a value provider, an injection token is necessary.But why do we need it? Each dependency in an injector has to be described by a unique identifier — a token — so that Angular knows what should be resolved. For classes, as well as services, a token is a reference to the class itself. But what if the dependency is not a class, but an object, or even a primitive type? We cannot use an interface as a token, because such a construct does not exist in JavaScript — it will be removed during transpilation. Theoretically, we can use a string as a token:

However, this solution has a number of drawbacks. It’s very easy to imagine making a typo or accidentally using the same value for different dependencies. This is where the InjectionToken comes to the rescue:

The value given as an argument to the constructor is not an identifier, but a description — the identifier created by InjectionToken is always unique.

As you can see from the example above, we use the @Inject() decorator by passing a reference to the token in question as an argument.

If we want the token to globally represent a value and be tree-shakeable, we can additionally use the option object:

Another parameter we can configure in the provider is multi. Setting its value to true allows us to bind multiple dependencies to a single token and return them as an array. This prevents the default behavior of overwriting dependencies. To illustrate this, let’s create a token to which we will then assign two values. Here is the result we get:

One of the most common use cases for this are interceptors. According to the Single Responsibility Principle, each interceptor is responsible for a different action, and a multi-provider allows each interceptor to act even though they use the same token.

Forward Ref

The forwardRef function is used to create indirect references that are not resolved immediately. Since the order in which classes are defined matters, it is particularly useful when references are looped or when a component tries to use a reference to itself in its configuration:

Additional Benefits of Dependency Injection

In addition to code modularity and greater flexibility, creating loose dependencies makes testing easier. Replacing dependencies with their mock counterparts allows us to isolate the functionalities being tested and check their behavior in a controlled environment. Admittedly, testing frameworks take care of this for us, but in the case of complex services we can mock dependencies manually:

A design pattern that we can use using Dependency Injection is port-adapter. It assumes that one module defines the shape of an abstraction and another provides its implementation. This allows us to decouple logic and loosen dependencies between modules, since the implementation can be swapped on-the-fly. This is where an abstract class that creates an interface and can also be used as a token works great:

Conclusions

Dependency Injection is not only a programming technique, but also an application design philosophy that promotes solutions that are modular, flexible and easy to test. In this article, we discussed key aspects of DI in the context of Angular. Using Dependency Injection brings a number of benefits, including increased code readability, easier dependency management and flexibility in modifying applications. We encourage you to experiment with its use in your own projects and to deepen your knowledge of the best practices. Let Dependency Injection become an integral part of your approach to developing applications in Angular, bringing benefits both in the short and long term.

About the author

Milosz Rutkowski

Angular Developer at House of Angular. Web application development enthusiast with a zeal to learn more and more about Angular. Fan of clean code, good practices and well composed architecture.

Don’t miss anything! Subscribe to our newsletter. Stay up-to-date with the latest trends, tips, meetups, courses and be a part of a thriving community. The job market appreciates community members.

Leave a Reply

Your email address will not be published. Required fields are marked *