Angular's dependency injection framework makes it easy to share services across many different components and classes. However, there is a bit of a learning curve when it comes to properly utilizing it. In this article, we'll introduce the core concepts, then discuss how to utilize dependency injection with Angular using some code examples. Before proceeding, if you would like to learn more about what dependency injection is, please visit our introduction to dependency injection.
Dependency Injection in Angular
In Angular, dependency injection is the act of making a dependency available to a class which needs it to operate. A few common examples include:
- Creating a service which gets a list of store items, then injecting that service into a component which displays the list of store items.
- Making a service which sends the contents of a contact form to the backend, then injecting that service into the component which displays the contact form.
- Making a service which logs http errors, then injecting that into a service which makes backend calls.
In the examples above, there are always two parts: the service to be injected as a dependency, and the component or service to receive that dependency. Behind the scenes, there is a third part: Angular's dependency injection framework, which automatically handles injecting dependencies when they are registered appropriately.
Services and Dependencies
Angular provides functionality to inject services or objects as dependencies. In this article, we will focus on injecting services. A service is a class which provides some type of useful, well defined functionality. Services can be injected into components and other services, whereas components and modules cannot be injected into classes at all. In order to inject services, we must mark that the service is injectable.
Making a Services Injectable
Angular provides a class decorator which can be used to mark a service as a dependency which can be injected. The simplest way to use this is shown in the example code below, which uses the @Injectable decorator and passes in an object { providedIn: 'root' }
:
The providedIn: root declaration indicates that this service can be injected anywhere without any additional module import, export, or provider specifications. This is generally the appropriate way to proceed, but in certain cases, it may be useful to require the consumer to import a certain module before being able to use this service. In this case, the decorator can be used, but instead of declaring { providedIn: 'root' }
, you can declare it as {}
, then export the service in whichever module is exists in. However, such a use case is very uncommon.
Injecting a Service
Angular injects dependencies via constructor dependency injection, which means that the class will receive the dependency as soon as it is instantiated. To have a consumer receive the injectable dependency, we will need to declare the service as a dependency in the constructor itself. In the code below, we will create a component which receives the ExampleService as a dependency.
In the code above, the dependency is declared using three parts:
- private: this indicates the property will be a class property. This is equivalent to declaring a private variable above the constructor, then assigning it within the constructor.
- variable name: in the example above, the variable name is exampleService (with a lowercase e). This is the name of the class variable the service will be assigned to and the handle we will use to invoke the service.
- service name: in the example above, the service name is ExampleService (with a capital E). This is the class name of the service which will be injected.
Angular recognizes the service name based on the type definition declaration: private [varName]: [typeName]
. Therefore, the service name declaration must be the same as the class name of the service. The variable name does not need to be the same, but the recommended convention is to make the variable name the same as the service name with a lowercase first letter, similar to what was done in the example above.
Services as Singletons
When a service is injected for the first time, Angular will take care of creating the new instance of the service and injecting its own dependencies. By default, once the instance is created, Angular will reuse that instance for future consumers which require the dependency instead of creating new instances for each consumer. This will enable you to store state on the service itself and facilitate reactions to changes in state in the consumers. For example, if one component has the service load some data, at which point the service caches it, another consumer or service which needs that data can receive the cached data.
Class vs Interface Based Declaration
Unlike dependency injection frameworks in other languages, Angular with TypeScript does not provide interface based injection. For example, Angular's dependency injection would not be able to handle the following declaration:
Fine Tuning and Substituting Dependencies
Angular also provides more sophisticated control over dependency injection. For example, you can register dependency substitutions via dependency providers, so when one class is requested, Angular will instead provide a different class. This allows for polymorphic behavior and similar behavior to that which can be achieved via interface based declaration. However, a detailed discussion falls outside of the scope of this article.