This is image title
What DI provides:

  • Sharing functionality between different components of the app
  • providing mocks instead of real connections when unit testing
  • not bothering about instances of classes
  • Having a clear understanding of the dependencies of a given class

But aside from providing services for fetching data, the Angular’s Dependency Injection Mechanism allows us to achieve some higher level of decoupling in our apps, and in this article I’m going to explore how.

If you are new to Angular or unfamiliar with the Dependency Injection, I suggest you first read what the official documentation has to say on the topic.

You may also like:Angular Dependency Injection Provider Objects

Providing the environment variables

If you have been using Angular for a while, chances are you are familiar with environment.ts files. These files are used to provide information about the environment in which our app is running. For example, if our app is running in a development environment, we want our data fetching services to point at the server app running on [http://localhost:3000/api](http://localhost:3000/api,), for example, and if it is running on the temporary server for the manual QA, we could want it to point to [https://qa.local.com/api](https://qa.local.com/api,), for instance. This is managed by Angular during build process with the usage of different environment files, for example, we could have two files called environment.ts and environment.qa.ts in our environments folder, and when we run the command ng build --config qa, the Angular CLI will replace our environment.ts file with environment.qa.ts and the app will run in the QA mode respectively.

But what does DI have to do with this?

Take a look at this component:

import { Component, OnInit } from '@angular/core';
import { environment } from 'src/environments/environment';

@Component({
  selector: 'app-some',
  templateUrl: './some.component.html',
  styleUrls: ['./some.component.css']
})
export class SomeComponent implements OnInit {

  constructor() { }

  ngOnInit() {
    if (!environment.production) {
      console.log('In development environment') {}
    }
  }

}

A naive usage of the environment variables
Here we just import the environment.ts file and use its contents right away.

But what if we want to inject some other values inside this environment variables for unit testing? Also, have a look at this service that also makes use of the environment variables:

import { environment } from 'src/environments/environment';

@Injectable()
export class SomeService {

  constructor(
    private http: HttpClient,
  ) { }

  getData(): Observable<any> {
    return this.http.get(environment.baseUrl + '/api/method');
  }

}

Using environment variable to point at an API

Again, this looks very normal, but in reality this is not the best way to reference an environment variable. Imagine that this service is a part of a multiple project Angular application. So we have a projects folder, which contains different apps (web components, for instance, is a good example of such a complex app). Can we reuse this service in another project? Theoretically we can — just import it into another Angular module using the providers array, right? But here comes the problem — different projects can have different environments! We cannot just import one of them, but we still have to ensure that this service can be reused by as many components as possible. We can achieve this by using DI.

There are several possible ways of achieving this functionality, but let’s start from the most simple one — Injection Tokens.

Injection tokens are an Angular concept which allow us to declare independent unique dependency injection tokens to inject values into other classes using the Inject decorator. Read more about them here.

All we have to do is just provide the value for our environment in a module:

export const ENV = new InjectionToken('ENV');

@NgModule({
   declarations: [
      AppComponent,
      SomeComponent
   ],
   imports: [
      BrowserModule
   ],
   providers: [
     {provide: ENV, useValue: environment}
   ],
   bootstrap: [
      AppComponent
   ]
})
export class AppModule { }

As you see, we have provided the value for our environment variable under an InjectionToken, so now we can use it in our service:

import { Injectable, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

import { ENV } from '../app.module';

@Injectable()
export class SomeService {

  constructor(
    private http: HttpClient,
    @Inject(ENV) private environment,
  ) { }

  getData(): Observable<any> {
    return this.http.get(this.environment.baseUrl + '');
  }

}

Notice how we no longer import the environment.ts file. Instead we bring the ENV token and allow the app itself decide what value will be passed on to the service, depending on the project in which it is being used.

But still, this is not the best solution. We did not provide a type for the environment private field, which is kind of self defeating — we need typings to make our code less error prone, and Angular can leverage typings to bring better DI — for example, we actually can get rid of the Inject decorator and the InjectionToken as a whole! Here’s what we are going to do: write a shell class to describe the interface of our environment variable, and then use it to provide the actual value. Here is an example of such a class:

export class Environment {
  production: boolean;
  baseUrl: string;
  // some other fields maybe?
}

In this class we described what our environment actually looks like. But we can also use this class as an Injection Token for injecting the environment variable! Just using useValue as a provider:

@NgModule({
   declarations: [
      AppComponent,
      SomeComponent
   ],
   imports: [
      BrowserModule
   ],
   providers: [
     {provide: Environment, useValue: environment}
   ],
   bootstrap: [
      AppComponent
   ]
})
export class AppModule { }

And in our service:

@Injectable()
export class SomeService {

  constructor(
    private http: HttpClient,
    private environment: Environment,
  ) { }

  getData(): Observable<any> {
    return this.http.get(this.environment.baseUrl + '');
  }

}

This solution not only provides us with a cleaner, more usual DI mechanism, but also with static typing.

But there is still a very small problem. Consider this:

@Injectable()
export class SomeService {

  constructor(
    private environment: Environment,
  ) { }

  someMethod() {
    this.environment.baseUrl = 'something else';
  }

}

Here we change the value of one of our environment variables. And because Angular modules make sure that within itself, every component gets the same instance of a dependency, doing this:

export class SomeComponent implements OnInit {

  constructor(
    private someService: SomeService,
    private environment: Environment,
  ) { }

  ngOnInit() {
    this.someService.someMethod();
    console.log(this.environment.baseUrl);
  }

}

Will yield this:

angular

So we “accidentally” changed an environment variable. This is not a very big concern (developers don’t usually go around randomly reassigning environment variables), but we still want to reinforce that rule. It is pretty easy — just make the fields on our class readonly:

export class Environment {
  readonly production: boolean;
  readonly baseUrl: string;
  // some other fields maybe?
}

Now we have the full power of DI, but are also protected against accidents.

Providing different services depending on the environment

We now know how to provide the environment variables via DI, but what about switching between different services (while preserving the API) depending on the environment?

Imagine the following situation: we need some crash reports/usage statistics from our application when it is running. But the nature of the logs differs depending from the environment we are using: in the development environment we want to just log an error or a warning to the console; in the QA environment, we want to call an API which will collect our errors in an Excel file and send them to the management; and in production, we want a separate log file on the backend, so we call another API. So we come up with an idea of having a Logger service, which will handle this functionality. Here is a naive implementation of this service:

@Injectable()
export class LoggerService {

  constructor(
    private environment: Environment,
    private http: HttpClient,
  ) { }

  logError(text: string): void {
    switch (this.environment.name) {
      case 'development': {
        console.error(text);
        break;
      }
      case 'qa': {
        this.http.post(this.environment.baseUrl + '/api/reports', {text})
                 .subscribe(/* handle http errors here*/);
        break;
      }
      case 'production': {
        this.http.post(this.environment.baseUrl + '/api/logs/errors', {text})
                 .subscribe(/* handle http errors here*/);
      }
    }
  }
}

This is pretty straightforward: receive an error text, check the environment, perform the respective action. But here are some problems I have with this solution:

  • We are making checkings every time the logError method is called. This is pretty useless, because after the app is built, the value of enviroment.name never changes! The outcome of the switch statement is always going to be the same, no matter how many times or in what situations we call this method.
  • The implementation of the method itself looks pretty ugly, it is not as straightforward as it may seem.
  • What if we need to log more different information? Do we have to write all these checkings in every method?

What do I suggest? Write separate LoggerServices for each scenario, and inject only one of them conditionally, depending on the environment, using a factory. Here’s how:

export class LoggerService {
  logError(text: string): void { }
  // maybe other methods like logWarning, or info
}

@Injectable()
class DevelopLoggerService implements LoggerService {
  logError(text: string) {
    console.error(text);
  }
}

@Injectable()
class QALoggerService implements LoggerService {

  constructor(
    private http: HttpClient,
    private environment: Environment,
  ) {}

  logError(text: string) {
    this.http.post(this.environment.baseUrl + '/api/reports', {text})
                 .subscribe(/* handle http errors here*/);
  }
}

@Injectable()
class ProdLoggerService implements LoggerService {

  constructor(
    private http: HttpClient,
    private environment: Environment,
  ) {}

  logError(text: string) {
    this.http.post(this.environment.baseUrl + '/api/logs/errors', {text})
                 .subscribe(/* handle http errors here*/);
  }
}

A breakdown of what we have done:

  • We strip the LoggerService down to just the declaration of it’s API, no implementation. We are going to use it as an injection token, and also as a hint for TS to know its interface.
  • We create separate classes for each environment, making sure to implement LoggerService on each of them, so we have the same API interface.
  • Each class performs the same methods, but in different ways. No need to check the environment.

Okay, but how are we going to tell our components what version of the LoggerService to use? Here is where factories come in play.

A factory is a pure function which receives dependencies as arguments and returns a value which will be provided for a given token. Here is how we are going to provide our LoggerService:

export function loggerFactory(environment: Environment, http: HttpClient): LoggerService {
  switch (environment.name) {
    case 'develop': {
      return new DevelopLoggerService();
    }
    case 'qa': {
      return new QALoggerService(http, environment);
    }
    case 'prod': {
      return new ProdLoggerService(http, environment);
    }
  }
}

This is a function which will provide one of the Services depending on the runtime value of the environment variable, and do so once. Which is a very important thing, considering we wanted to reduce clutter in our code, and don’t perform unnecessary checkings.

But of course this is only part of the solution: we still need to tell Angular to use our factory and provide the necessary dependencies via the deps array.

@NgModule({
   providers: [
     {
       provide: LoggerService,
       useFactory: loggerFactory,
       deps: [HttpClient, Environment], // we tell Angular to provide this dependencies to the factory as arguments
    },
     {provide: Environment, useValue: environment}
   ],
   // other metadata
})
export class AppModule { }

The greatest thing about this solution is that we don’t need to change anything else in our app: all the components that used the LoggerService will continue to do so, as if the LoggerService still contains the actual implementation.

export class SomeComponent implements OnInit {

  constructor(
    private logger: LoggerService,
  ) { }

  ngOnInit() {
    try {
      // do something that may throw an error
    } catch (error) {
      this.logger.logError(error.message); // no need to change anything - works the same wy as previously
    }
  }

}

Providing global singletons

Angular makes sure that inside a given module, all components receive the same instance of a dependency. For example, if we provide a service on AppModule, then declare a SomeComponent on that module, and inject SomeService into it, and also into AnotherComponent declared in that same module, both SomeComponent and OtherComponent will receive the same instance of SomeService. But in different modules it is not going to be the same: each module has its own dependency injector, and will spawn different instances of the same service for different modules. But what if we want the same instance for every component, service or whatever that tries to use our dependency? So we, essentially, want a Singleton.

We can have a singleton using the useFactory again. At first we will have to implement a static getInstance method on our class, just like with usual singletons, and then call it from out factory. Let’s say we want to implement a simple runtime data store, like very basic redux. I won’t go into details and only implement the getInstance method:

export class StoreService {

  // maybe store methods like dispatch, subscribe or others

  private static instance: StoreService = null;
  static getInstance(): StoreService {
    if (!StoreService.instance) {
      StoreService.instance = new StoreService();
    }

    return StoreService.instance;
  }

}

Nothing fancy here, but we have to tell Angular to call our getInstance method:

export function storeFactory(): StoreService {
  return StoreService.getInstance();
}

@NgModule({
   providers: [
     {provide: StoreService, useFactory: storeFactory}
   ],
   // other metadata
})
export class AppModule { }

And it’s done. Now we can just inject StoreService into any component anywhere and be sure we have the same instance. We can also provide dependencies to the factory as usual.

General advice on DI

Here are several short rules on how to work with DI in Angular:

  1. Always inject every value into your component, never rely on global variables, variables from other files and so on. As a rule of thumb, if you find any method of your class referencing anything other than properties from that class or local variables, change your class s it receives that value as an injected dependency (like we did with the environment variables).
  2. Never use string tokens for DI. In Angular it is possible to give the Inject decorator a string for it to look up the dependency. Never do that — a typo is always a possibility. Also, you cannot rely on IntelliSense to autocomplete it for you. Use an InjectionToken instead.
  3. Remember than instances of services are shared between components at least on the module level, so if any properties on that services are not meant to be modified from the outside world, consider marking them as readonly.
  4. If you are using a class to be provided instead of another one, make sure you implement them like we did in the second example. This way if the replaced dependency’s interface changes, we will have to reimplement the class that is meant to replace it, so we don’t run into cryptic errors.

This is only some part of the power that Angular’s Dependency Injection mechanism can give us. So it is very important to understand it thoroughly to write quality code.

Originally published by Armen Vardanyan at codeburst.io

#angular #web-development

Angular Dependency Injection Tips for Developers
5 Likes37.65 GEEK