Photo by Ashim D’Silva on Unsplash

Understanding Angular — Exploring Dependency Injection and Design Patterns — Part 1 🔥🚀

Giorgio Galassi

--

This is the 2nd of a series of articles that have the goal to explore and understand a topic that i see too much neglected in the front-end world — Dependency Injection (DI) & Design patterns.

Before starting a new ride with this article i suggest to have a look at the Part 0, which will covers the basics of the DI.

How does it work within Angular?

The concept of DI is the same across all the Programming Language, the “how to” can of course differ from language to language.

In Angular the setup that you have to do is pretty simple.

Create a Class — aka a Service as follows:

import { Injectable } from '@angular/core';

// We are telling Angular that at some poin this service may be injected
// Using the @Injectable() decorator
@Injectable()
export class MyAwesomeService {}

Angular provides a lot of the so called Decorators which are used to dynamically add different behaviors to an object at run time. Decorators are not related to Angular only. They are a well known design pattern.

Anyway, along this journey you will learn about some specific design patterns that will make use of the DI, so stick around!

That’s pretty straight forward, now you need to understand…

How can we inject a dependency?

You have two ways to inject a service, actually.

The constructor way

As you can see looking at the code below you need to create a variable with an arbitrary name — please be clear as possible when you name you variables — and explicit the type of that variable using the MyAwesomeService class that you previously created.

import { Component, inject } from '@angular/core';

import { MyAwesomeService } from './providers/my-awesome.service';

// This can be a @Directive() or a @Pipe() as well
// You can inject (using this way or the next one) service within service
@Component({...})
export class MyAwesomeComponent {
constructor(private _myAwesomeService: MyAwesomeService) {}
}

The Inject way

You have to create a variable here as well, as you can notice. The main difference is that in the Inject way you will use the inject() function provided by the Angular framework. The argument of this function is the MyAwesomeService class itself, from which the Angular will infer the type in our variable.

import { Component, inject } from '@angular/core';

import { MyAwesomeService } from './providers/my-awesome.service';

@Component({...})
export class MyAwesomeComponent {
public _myAwesomeService = inject(MyAwesomeService);
}

There are several advantages to use the Inject way, which is my personal favorite, but we are not going to deep dive in these advantages yet (maybe a next article, who knows?).

Regardless of the way that you will use the Injector will behave in the same way:

It will search for a instance of that specific class — MyAwesomeService — and will inject it to the Client that requested the instance.

Everytime you create a Service you to decide in which way you want your Service to behave and ask your self this simple question…

Do I want a Singleton?

Let’s start with the definition.

A Singleton is a Service that has been instantiated once and this instance is shared across the whole application, therefore that data stored in this Service will be the same regardless of where you access it from.

You have two ways in which you can force your service to behave like a Singleton:

AppConfig way

Looking the screenshot below you can notice how the MyAwesomeService class is provided within the appConfig’s providers array.

In this way you are telling Angular that your service must be instantiated at a global level and that it’s instance will be available in the Root Injector (you are going to understand what the Root Injector is seconds).

import { ApplicationConfig } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

import { AppComponent } from './app/app.component';
import { MyAwesomeService } from './app/providers/my-awesome.service';

export const appConfig: ApplicationConfig = {
providers: [
{
provide: MyAwesomeService,
},
],
};

bootstrapApplication(AppComponent, appConfig).catch(console.error);

Injectable.provideIn way

The second way to achieve a Singleton behavior is by using the provideIn property provided by the @Injectable() annotation that you used before to tell Angular that your service will be, at some point, injected.

By setting this property value to root you are telling to Angular to put the instance of this service, as the previous way, in the Root Injector.

import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root',
})
export class MyAwesomeService {}

Now… why do we have two ways to achieve the same thing? Do i get anything from using the first or the second? You may ask.

Well, yes. The second way it’s actually the go to for a very specific reason…

Tree Shaking

Before everything let’s see another definition:

It’s a process that removes, from our bundle, unused code. This result in a smaller bundle.

This process is possible only if you set provideIn property. Angular is not able to understand of the service has been used anywhere within our Application if the Service is listed in the appConfig’s providers array — like you would do using the first way.

It’s time to dive way more inside the Angular’s environment and understand how your service will be injected. Let’s have a look to the…

Injectors Hierarchy

The Injector has been mentioned before in this article and the moment to discover what it is has arrived.

We have two hierarchies:

EnvironmentInjector hierarchy

In order to create a Service in this hierarchy you can use the @Injectable() annotation or the providers[] provided by the ApplicationConfig object.

ElementInjector hierarchy

This type of Injector is implicity created for each DOM element. It’s empty by default unless you say the opposite. To do so, you have to declare a Service within the providers[] provided by the @Directive() or the @Component() annotation.

There are two more EnvironmentInjector that are above the root injector. Another EnvironmentInjector and the NullInjector. Have a look to the diagram below to better understand how they are related and what they do!

Injectors hierarchy

The NullInjector is the responsible of the error below, everyone of us, saw at least a billion times! Now you know who to blame, after yourself! 😎

NullInjector no provider error

Now you understood how the Injector’s hierarchy is composed. The last step on the list is to see — with a nice little graph — how the flow works and…

What happen when we ask Angular to Inject a Service?

Let’s image to start your DI journey to the ChildComponent, marked with that tiny little syringe.

Angular will start to search for the requested Service’s instance in the ChildComponent’s ElementInjector, if nothing is there will go up to the DI tree, doing the same thing with the ParentComponent.

If nothing is found, the request will go back where it started — in our case the ChildComponent — and the same request will be sent to the root EnvironmentInjector.

In the unlucky case that even in the 1st EnvironmentInjector there is not an instance of the requested Service the platform EnvironmentInjector will be asked to provide something that can match the reuqest if is not possibile the NullInjector will thrown an error.

The injection request can be fulfilled at any point of the flow, based on where the Service has been provided as you saw earlier in the article.

DI flow

This flow is pretty straight forward and Angular has the total control over it. Unless you, a brave developer, want otherwise.

Say hello to the…

Resolution Modifiers (RM)

These modifiers are not other than annotation that, as the name suggest, allows you to change how Angular will look for a specific Service’s instance and what to do if no instance has been found.

To tell Angular what to do in case it’s not able to find an instance of a Service to fulfill the injection request we have only…

@Optional()

This modifier will prevent Angular to throw an error. Be sure to guard your code against a null value — an if-clause will be enough.

You can also tell Angular where to start looking for an instance. Even in this case you have only one RM, the…

@SkipSelf()

Angular by default start to look at the current Injector, using this modifier Angular will start from the parent Injector.

If is possible to tell Angular where to start looking, is possible to say also where to stop looking. In this case we have two RM

@Host()

Angular will stop looking for an instance up to the component where @Host() is placed.

@Self()

Angular will stop looking for an instance in this current Injector.

These concepts may be complex at first, you need to play with the RMs in order to fully understand how and when use them.

For this purpose i will leave below a very simple project in which in you can play as much as you want.

Conclusions

Let’s recap what you saw in this article, because it’s a lot!

You started from the Dependency Injection. Went straight to see how injection works. Flew over different types of Injectors and their hierarchies to land over the Resolution Modifiers.

All this knowledge may seems overwhelming at first, but the more you will play with this knowledge the more clearer it will be.

Make sure that all of this is engraved in your mind, because we are going to see more in the next articles of the series and I want you to be ready! 💪🏻

May the DI be with you!

Thanks for sticking with me and hope that everything is clear so far.

The journey will continue and I will be glad if you want to enjoy the ride with me till the last article of the series and maybe explore more of mine.

See you in the next one,
G.

--

--

Giorgio Galassi

Angular developer. 💻 Proud member and manager of Angular Rome community! 🚀