Demystifying Dependency Injection in Angular

Airnauts
5 min readFeb 21, 2023

--

text by Mikolaj Krzyszczak and Pawel Sienkiewicz, visuals by Nikita Sergushkin

If you are an Angular developer, you must have seen this error message at least once:

Usually it’s quite easy to solve, basically a one-line fix, but it is still worth turning this problem upside down and understanding its nature. Before we do that, let’s recap some basic terminologies related to Dependency Injection (DI) and its place in the Angular world.

DI in Angular is one of its central built-in features. It is a design pattern that promotes separation of concerns and makes it more convenient to test, maintain, and modify the code.

In Angular’s DI we can distinguish a few main roles: injector, injected dependency, dependency consumer and dependency provider.

Dependency consumers

A DI consumer can be any class with an Angular decorator( such as @Component(), @Directive(), @Pipe(), @Injectable()) that acquires the resources it needs to function, rather creating them.

Injectors

An injector is a key part of the DI mechanism in Angular. It handles dependencies and returns requested values. To make this functionality fully configurable Angular introduced the ability to have multiple injectors in an application.

The types of injectors available are the following:

  • Null Injector
  • Platform and Root Injector (special types of Module Injector),
  • Module Injector,
  • Element Injector.

Another important aspect of Angular’s dependency injection system is the concept of “hierarchical injection” which allows for more fine-grained control over the dependencies that are made available to different parts of the application.

Injectors are organized in a tree-like structure:

Source: https://www.youtube.com/watch?v=G8zXugcYd7o&t=21s&ab_channel=DecodedFrontend
Source: https://www.youtube.com/watch?v=G8zXugcYd7o&t=21s&ab_channel=DecodedFrontend

NullInjector() is the root of the tree and parent of every injector. Its main role is to throw NullInjectorError, when Angular DI is looking for a dependency in that service.

Platform injector is a specialized sub-type of Module Injector. It is created by the platformBrowserDynamic() and configured by a PlatformModule, which contains platform-specific dependencies. It gives the possibility to share platform configurations between multiple applications.

Root injector is created when an application is getting bootstrapped. It can be configured by providers from NgModules. This Module Injector flattens all providers arrays that can be directly accessed using NgModules.imports property.

For each lazy-loaded module, a new instance of Module Injector will be created.

Element Injector will be created for each DOM element. To create an instance for a specific component and its children we need to provide a service that we want to inject in any decorator such as @Component or @Directive that has providers or view providers properties.

Injected dependency

Dependency can be anything. Typically they are services(classes), but you can provide values such as string, functions, or other data types (from primitives to complex objects). When a class or service needs to be provided in DI context it is quite straightforward and all we need to do is to add them to the providers array. This scenario becomes a little bit more complicated when it comes to injecting primitive data types or values that don’t have runtime representation such as: Interfaces or Arrays. Luckily Angular also has a solution for this and it’s called Injection Token.

Injection token is a class that gives the possibility to inject any value or data type that you can think of. It is parameterized, which gives the developer the possibility to set the type of object that will be returned by the Injector.

Dependency provider

Angular DI mechanism provides the necessary APIs to make configuration flexible, so the developer has full control over which values will be available for use. The role of dependency provider is to configure an injector with the proper provider token that is used for creating instances of the dependencies that are injected into components, directives, pipes or different services.

To make a dependency available for injection it needs to be registered with an injector. This can be done in two ways — using the providers property of the @NgModule decorator or inside the components metadata section. The property mentioned above is just an array of provider tokens, which are used to identify the dependencies that need to be made available for injection.

The simplest and usually most used case is the default provider called TypeProvider.

As we can see in this example, an array of providers consists of a single service class delivered as a provider token meant to be injected. Its default behavior is to instantiate that class using the new (class constructor) operator.

But what is really the difference between specifying the provider token inside ngModule vs. the component decorator? Using the providers array in the component allows the developer

to override providers defined in the parent’s injector by providing the same provider in the child’s injector. This allows for finer-grained control over the dependencies that are made available to different parts of the application. For example, when a component is loaded in a lazy-loaded module, it can use a different instance of a dependency than the one used by a component in the main module.

The previous example demonstrates very basic usage of the provider interface and there are a couple of ways to expand the provider configuration which is object literal with just two properties. The provide property is responsible for holding the token which serves as a key for locating the corresponding dependency value and configuring the injector. The second one is a provider definition object which describes how an injector should create or deliver the dependency values. We can distinguish 4 approaches for creation of mentioned values:

  1. useClass — used for providing an alternative implementation of class declared as ‘provide’. Good for extending existing class or mocking services for test cases.
  2. useExisting — lets developers map a provider token as an alias for service with a different token
  3. useFactory — creates a dependency object by calling a factory function.
  4. useValue — links a static value with a token

As we know Dependency Injection is a very complex mechanism and this article scratches just the surface of this topic. If you find yourself interested in learning more or have a domain-related question, feel free to ask The Airnauts Angular team is always keen to help.

--

--

Airnauts

Enabling forward thinkers to turn great ideas into greater results through technology & creativity.