DEV Community

Cover image for A Practical Guide to Providers in Angular
Alisa for Angular

Posted on

A Practical Guide to Providers in Angular

When creating apps with Angular, you can add and configure dependencies for the application you’re building using something called "providers.” You use the built-in Dependency Injection (DI) system to create providers. This post will cover Angular’s powerful DI system at a high level and demonstrate a few practical use cases and strategies for configuring your dependencies. Let’s get practical!

Table of Contents

Quick overview of Dependency Injection

Dependency Injection decouples the creation of a dependency from using that dependency. When you use DI, it promotes loose coupling within our code - a foundation for creating well-architected software. Practicing good software design patterns yields flexible, maintainable software that allows our applications to grow with new features more quickly. And by using DI, we can change the dependent code without changing the consuming code! Seamless code switches are nearly impossible with tightly coupled code, where you might have to touch everything to make a small change.

The cool thing is Angular has DI built-in and helps set us up for success. How handy!

Angular’s Dependency Injection system

When you use the Angular CLI to generate a service, it automatically adds the code to register the service within Angular’s DI system for us. 🎉 Services contain business logic code that we want to keep separate from view logic.

When Angular CLI generates a service, it adds an @Injectable() TypeScript decorator, which is the bit of code that registers a service within the Angular DI system:



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


Enter fullscreen mode Exit fullscreen mode

Without doing anything else, we can use our dependency in the application by injecting it into the consuming code as a constructor parameter:



@Component({
  // standard component metadata here
})
export class MyComponent {
  constructor(private myService: MyService) { }
}


Enter fullscreen mode Exit fullscreen mode

In Angular v14, you have a new option to use the inject() function instead of injecting the service into the consumer as a constructor parameter.

Angular CLI is 💯! The generated service allows you to start using your service immediately, and the Injectable() TypeScript decorator is tree-shakeable so it's an all-around win!

Another way to register dependencies is to provide them manually through the providers array. Different Angular building blocks accept providers in the metadata. So you can register a provider like this:



@NgModule({
  imports: // stuff here
  declarations: // stuff here
  providers: [
    MyService
  ]
})
export class AppModule {
}


Enter fullscreen mode Exit fullscreen mode

There’s something else to note, though. Angular’s DI system allows you to provide a dependency to different places within the application. We saw this example in the first code snippet of the @Injectable() TypeScript decorator. Angular CLI automatically generates:



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


Enter fullscreen mode Exit fullscreen mode

The configuration option providedIn: 'root' specifies where within the application to provide the service. In this case, we’re saying provide to “root,” which means the application's root. When you provide at the root of the application, it a single instance of the service is available across the entirety of the app.

The Injector

The Injector is the mechanism for handling DI. It manages the dependencies and gives you the dependency you request. Angular has multiple injectors, and the injectors are hierarchical. 💫 There are different categories of injectors - Module Injectors, Element Injectors, and a special fallback injector called the Null Injector. Whenever I think of a hierarchy, I can't help but think about a tree. 🌳 So we can visualize the injector hierarchy like this.

Injector hierarchy on an upside down cartoon tree graphic

Going back to the example we had above when we use the instruction providedIn: 'root', what we're doing is providing to a particular module injector, the Root Injector.

Providing to different injectors

You can configure the providers array in other modules and Angular building blocks, such as components and directives. This means you can create an instance of a dependency available to your module and everything in it or only to your component. You might consider these options if you need a 1:1 relationship between an instance of the dependency and the consumer, such as if you maintain state specifically for a module or component within your dependency. Be careful to avoid causing unnecessary complexity, though!

In Angular v14, you can provide to routes as part of your route paths definitions! Angular v15 deprecates a confusing option for the providedIn configuration called any used explicitly for lazy-loaded modules, as well as assigning specific modules in the configuration.

Because you can provide to so many different places, Angular has to resolve which instance of a dependency you get when you consume it. You’ll get the provider you configured closest to the consuming code as a general rule, and move up the tree to find the requested dependency.

Same image of tree with injectors with an arrow pointing up the tree to show the resolution path

Still, this resolution process is complex, and it can be difficult to figure out what's going on when using a dependency with such a complicated setup. Fortunately, the Angular team announced plans for better debugging tools that help us understand where a dependency comes from. 🎉

As with most things, the most straightforward, simplest approach is best. If you can get away with providing to the Root Injector so that you only have one instance of dependency for your application, then you should.

Decision chart showing to use 'root' if a dependency can be a single instance within the application, otherwise use module or element injectors

While having this level of configurability sounds unnecessarily complicated, it allows you to fine-tune which dependency to use in your consuming code. Now that we have a quick overview of how and where to provide dependencies let’s review an integral piece of Angular’s DI system, injection tokens.

Injection tokens in Angular

Injection tokens allow us to have values and objects as dependencies. This means we can depend on strings, such as “Hello world!” and objects, which include configuration objects and global variables, such as Web APIs. But injection tokens are even more remarkable because we can also create dependencies to constructs that don’t have a runtime definition, such as interfaces! Let’s take a look at an example using an injection token.

Let's say you work on a language learning application with a user configuration that includes the language the user is learning. You have an interface definition of the configuration as well as a concrete instance of the configuration:



export interface UserConfig {
  language: string;
}

export defaultUserConfig: UserConfig = {
  language: 'en'
}


Enter fullscreen mode Exit fullscreen mode

You can register the token to Angular's DI system and return the default configuration by providing the type, a description, and options like this:



const export USER_CONFIG_TOKEN = new InjectionToken<UserConfig>('userconfig', {
  providedIn: 'root',
  factory: () => defaultUserConfig
});


Enter fullscreen mode Exit fullscreen mode

When you want to use the USER_CONFIG_TOKEN, you will use the @Inject decorator:



@Component({
  // standard component metadata here
})
export class MyUserProfileComponent {
  constructor(
    @Inject(USER_CONFIG_TOKEN) private config: UserConfig
  ) { }
}


Enter fullscreen mode Exit fullscreen mode

Now we can access the user config from within the component! Accessing a config might not seem like a big deal, but we used injection tokens to inject an interface into the component! Having injection tokens as a means to represent values and interfaces as dependencies are enormous! And it sets us up to leverage the power of Angular’s DI system.

We can use injection tokens and configure providers within Angular’s DI system for more power and fine-grained control.

Configuring providers in Angular’s Dependency Injection system

You can configure the providers array to add fine-grained control to your providers. When combined with injection tokens, we can unleash a lot of power. But first, it’s essential to know when it makes sense to do so. Always prefer the most straightforward, default way of registering a dependency and then use fine-grained control as needed.

To configure the providers array, you add an object containing the instructions like this:



@NgModule({
  imports: // stuff here
  declarations: // stuff here
  providers: [{ 
    provide: MyService,
    howToProvide: OtherDependency
  }]
})
export class AppModule {
}


Enter fullscreen mode Exit fullscreen mode

The “how to provide” gives Angular-specific instructions on this dependency configuration. Then you can provide the other new dependency. Angular supports the following options for “how to provide”:

  • useClass - Replace the current dependency with a new instance of something else
  • useExisting - Replace the current dependency with an existing dependency
  • useValue - Replace the current dependency with a new value
  • useFactory - Use a factory method to determine which dependency to use based on a dynamic value

Next, let’s walk through examples of each configuration option to understand how to use them.

Configure providers with useClass

The useClass option replaces the current dependency with a new instance of another class. This is a great option if you’re refactoring code and want to substitute a different dependency in your application quickly. Let’s say you have a language learning app and an Angular service that wraps the authentication calls you delegate to an auth library and an auth provider. We’ll call this service AuthService, and keep the code straightforward like this:



@Injectable({
  providedIn: 'root'
})
export class AuthService {
  public login(): void { }
  public logout(): void { }
}


Enter fullscreen mode Exit fullscreen mode

In a stroke of luck, a large tech company decides to buy your language learning app, requiring you to authenticate using their social login only. You can create a new authentication service that wraps the calls to their auth provider and keeps the same member names; we’ll call it NewAuthService. (Note, you should not name your services with these terrible generic names. Be a bit more descriptive. )



@Injectable({
  providedIn: 'root'
})
export class NewAuthService {
  public login(): void { /* new way to login */ }
  public logout(): void { /* new way to logout */ }
}


Enter fullscreen mode Exit fullscreen mode

Because both classes have the same public members, you can substitute the original AuthService with the new NewAuthService by configuring the provider:



@NgModule({
  imports: // imports here
  declarations: //declarations here
  providers: [
    { provide: AuthService, useClass: NewAuthService }
  ]
})
export class AppModule { }


Enter fullscreen mode Exit fullscreen mode

The cool thing about having the same public members is that there’s no need to change the consuming code. Angular instantiates a new instance of NewAuthService and provides that dependency to consuming code, even if they still refer to AuthService!

It might not make sense to keep the original AuthService around, so you might want to consider transferring all the code references to use the NewAuthService only. However, the useClass configuration option is a fast way for us to quickly substitute one instance of a class for another, which means proofs-of-concept and quick checks can be super-fast!

Configure providers with useExisting

The useExisting option replaces the provider with a different provider already existing within the application. This option is a great use case for API narrowing, that is, decreasing the surface area of an API. Let’s say your language learning application has an unwieldy API. We’ll call this API LanguageTranslationService, and it looks like this:



@Injectable({
  providedIn: 'root'
})
export const LanguageTranslationService {
  public french(text: string): string { /* translates to French */ }
  public japanese(text: string): string { /* translates to Japanese */ }
  public elvish(text: string): string { /* translates to Elvish */ }
  public klingon(text: string): string { /* translates to Klingon */ }
  // so on and so forth, but you see the problem here
}


Enter fullscreen mode Exit fullscreen mode

And you consume the service like this:



@Component({
  // standard component metadata here
})
export class ElvishTranslationComponent implements OnInit {
  private elvish!: string;
  constructor(
    private translationService: LanguageTranslationService
  ) { }

  public ngOnInit(): void {
    this.elvish = this.translationService.elvish(someText);
  }
}


Enter fullscreen mode Exit fullscreen mode

Oops… The LanguageTranslationService looks a bit unwieldy. Let’s narrow the API surface by creating a new class called FictitiousLanguageTranslationService and move the translation methods for the fictitious languages there. We’ll use an abstract class for this:



export abstract class FictitiousLanguageTranslationService {
  abstract elvish: (text: string) => string;
  abstract klingon: (text: string) => string;
}


Enter fullscreen mode Exit fullscreen mode

Now we can add FictitiousLanguageTranslationService as a real dependency in the application by adding it to the providers array, but use the existing LanguageTranslationService implementation of the code:



@NgModule({
  imports: // imports here
  declarations: // declarations here
  providers: [{
    provide: FictitiousLanguageTranslationService,
    useExisting: LanguageTranslationService
  }]
})
export class AppModule { }


Enter fullscreen mode Exit fullscreen mode

Next, we’ll update the consumer to use the new dependency:



@Component({
  // standard component metadata here
})
export class ElvishTranslationComponent implements OnInit {
  private elvish!: string;
  constructor(
    private fltService: FictitiousLanguageTranslationService
  ) { }

  public ngOnInit(): void {
    this.elvish = this.translationService.elvish(someText);
  }
}


Enter fullscreen mode Exit fullscreen mode

Only the methods defined in the FictitiousLanguageTranslationService are available now. Pretty sweet!

Configure with useValue

The useValue option replaces the provider with a value. This option is a great use case for configurations and mocking services in automated tests where you need to control the inputs and outputs. Let’s go back to the USER_CONFIG_TOKEN in this example and override it to show a different language instead.

We can override the token:



@Component({
  providers: [{
    provide: USER_CONFIG_TOKEN,
    useValue: { language: 'jp' } 
  }]
})
export class MyUserProfileComponent {
  constructor(
    @Inject(USER_CONFIG_TOKEN) private userConfig: UserConfig
  ) { }
}


Enter fullscreen mode Exit fullscreen mode

Now when we use this in the MyUserProfileComponent we’ll see the user's language is Japanese instead of English!

Configure with useFactory

The useFactory option allows us to use a factory method to create a dependency. This option is a great use case if you have dynamic values to consider when creating the dependency. It’s also how we can use a factory pattern for creating our dependencies.

In this example, let’s say in your Language Learning application, if the user is learning Japanese, we want to show the Japanese flag in the MyUserProfileComponent instead of the default - a checkered flag. The user’s language selection is in the user’s config, so the example code looks like this:



@NgModule({
  imports: // imports here
  declarations: // declarations here
  providers: [{
    provide: USER_CONFIG_TOKEN,
    useFactory: (config: UserConfig) => config.language === 'jp' ? '🇯🇵' : '🏁',
    deps: [UserConfig]
  }]
})
export class AppModule { }


Enter fullscreen mode Exit fullscreen mode

Notice we were able to pass in a dependency to the configuration option. The useClass and useFactory options support passing in dependencies.

Now when we use the configuration in the MyUserProfileComponent we’ll get the Japanese flag instead of a checkered flag only if the user’s configuration has Japanese as their language!



@Component({
// standard component metadata here
})
export class MyUserProfileComponent {
constructor(
@Inject(USER_CONFIG_TOKEN) private config: UserConfig
) {
// flag is either 🏁 or 🇯🇵 based on the language setting
}
}

Enter fullscreen mode Exit fullscreen mode




Learn more about Angular Dependency Injection

This article offers a high-level overview of Angular’s DI system. As you can already see, it’s a powerful system with many different configuration options and complexity. As a result, even though Angular has these configuration options, using the most straightforward approach will make troubleshooting and maintenance easier! 🏆

Angular's documentation has great resources! There are many docs on Dependency Injection since it's such a broad topic. Here's where to get started - Dependency injection in Angular.


This post was originally published on the Okta Developer blog. I am the original author and made modifications.

Top comments (5)

Collapse
 
reytortugo profile image
ReyTortuga

Very well exaplined! Love it! Congrats! :D

Collapse
 
alisaduncan profile image
Alisa

Thank you for your kind words! 🥰

Collapse
 
jwp profile image
John Peters

Nice article thanks. I was a little surprised that Angular 14 examples like this are still using the NGModule system. Any thoughts on that?

Collapse
 
alisaduncan profile image
Alisa

Hi there! NgModules are still valid syntax in Angular v14, and Standalone components are in developer preview until Angular v15. I think we'll be seeing examples using both patterns for a while. While I called out Angular v14 & v15 changes, not everyone is on the latest versions.

I can definitely see a separate post that covers Standalone components and some of the new capabilities from a DI perspective though. 🙂

Collapse
 
reejesh_pk_abs profile image
Reejesh

Nice article, its clear with good examples. For those who are wondering what is klingon, kindly watch Big Bang Theory or Young Sheldon.