Learn how to use services and dependency injection to improve your Angular development by making it modular, extensible and loosely coupled.
Angular is a framework for building dynamic client-side applications using HTML, CSS, and JavaScript. It has a nice CLI tool that helps with developer productivity, and for generating code that follows the recommended Angular design guide so you can build fast, responsive and modular applications. In this article, I'm writing about services and dependency injection in Angular.
If you want to continue reading, you should already have an understanding of components, directives, modules, and data binding in Angular. I'll use the Angular CLI to generate the needed files so an understanding of working with the Angular CLI is also needed. However, if you don't know those things, you're in good company because I've written about them 😃.
Here are the links to the articles I've written covering those topics:
The sample application which we'll build together while you go through the article builds on the sample application from the articles I listed above. If you have been reading and working along with me over those articles, you should have the complete code. Otherwise, you can download the project on GitHub. When you download it, you should then copy the content from src-part-3 folder into the src folder if you want to code along while you read.
Services is a broad term used in various development methodologies to refer to a function or group of functions designed to do something specific. You'll see it used in microservice architecture, service-oriented architecture, domain-driven design, and many others.
For example, let’s say you have a class that represents a bank account. This class has functions to tell you the balance, deduct and add money to the account. But, if you want to transfer funds from one account to another, you need a function that'll deduct from one account and credit another account. This functionality belongs to a service. It can be in a class with several other functions that don't fit in the account class but need to manipulate the account. Working with Angular and TypeScript, a service is typically a class with a well-defined purpose.
In order to build a loosely-coupled application and reuse code, it is best if you design your components to be lean and efficient. This means that the component's job should be to focus on the user experience and nothing more. A component should contain properties and methods for data binding, and delegate tasks such as fetching data and input validation to another class (a service). Doing it this way, we can also reuse that code or service in other components.
We're going to put the logic for data retrieval in a service. Add a new file in the src/app/expenses folder called expense.service.ts and put the code below in it.
import IExpense from "./expense";
export class ExpenseService {
getExpenses(): IExpense[] {
return [
{
description: "First shopping for the month",
amount: 20,
date: "2019-08-12"
},
{
description: "Bicycle for Amy",
amount: 10,
date: "2019-08-08"
},
{
description: "First shopping for the month",
amount: 14,
date: "2019-08-21"
}
];
}
}
This is a service which we'll use in places we need to retrieve expenses. We'll use this in the home component. Open src/app/home/home.component.ts, and after line 2, add the statement below:
import { ExpenseService } from "../expenses/expense.service";
Then declare a variable with the service class as the type, and update the constructor.
private _expenseService: ExpenseService;
constructor() {
this._expenseService = new ExpenseService();
this.expenses = this._expenseService.getExpenses();
}
expenses: IExpense[];
We initialized the service class and called getExpenses()
, assigning the returned value to the expenses
property. We removed the default value for expenses
and set the value using the service class, as you can see in the constructor. This is how we move the logic of data retrieval to a service, and we can reuse that function across components.
Dependency Injection (DI) is a design pattern by which dependencies or services are passed to objects or clients that need them. The idea behind this pattern is to have a separate object create the required dependency, and pass it to the client. This makes a class or module to focus on the task it is designed for, and prevents side effects when replacing that dependency. For example, the home component's class depends on the ExpenseService
service to retrieve data. We don't want it to be concerned with how to create this dependency, so we delegate that to a DI container that knows how to create services and pass them to clients that need them. Using this pattern helps achieve loose coupling and increases the modularity of a software application, thereby making it extensible.
DI is also at the core of Angular and can be used to provide components with the dependencies that they need. You will need to register the service with the Angular DI system so it knows how to inject it into components that need it. An injector is responsible for creating the dependencies and maintains a container of dependency instances that it reuses if needed. The injector knows how to find and create dependencies through an object called the provider. During the application's bootstrap process, Angular creates the needed injectors so you don't have to create them.
To make a service injectable, you need to register it with a provider. There are three ways you can do this:
providers
option in the @Component()
metadata. Using this approach, each time the component is created, a new instance of the service is created and injected into it.
@Component({
selector: "et-home",
templateUrl: "./home.component.html",
styleUrls: ["./home.component.css"],
providers: [ ExpenseService ]
})
providers
option of the @NgModule()
metadata. With this approach, a single instance of the service is injected into clients that need it. For example, if the home
and briefing-cards
components need the same service and that service is registered at the module level, the same instance of that service is injected into the instance of home
and briefing-cards
.
@NgModule({
providers: [ ExpenseService ],
...
})
@Injectable()
decorator in the definition of that service.
@Injectable({
providedIn: 'root',
})
You can use the CLI to generate services. Using the CLI will create a service class and register it using the root provider by default. To use the CLI, you run the command ng generate service
. For example, we could have done ng generate service expenses/expense
to generate the ExpenseService
registered with the root provider.
You're going to register the ExpenseService
you created earlier, with the root provider.
Open the service file and add the statement below
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class ExpenseService {
.......
}
With this code, you referenced @Injectable
and used the decorator on the class definition.
For this service to be injected into the component, you specify it as a parameter in the component's constructor. Angular determines which dependencies a component needs by looking at the constructor parameter types. We'll update the home
component constructor so that the ExpenseService
service will be created and injected into the component.
Open src/app/home/home.component.ts and update the constructor definition as follows:
constructor(expenseService: ExpenseService) {
this._expenseService = expenseService;
this.expenses = this._expenseService.getExpenses();
}
When the component needs to be created and Angular discovers that the component has a dependency on a service, it first checks if the injector has any existing instances of that service. If an instance of that service doesn't yet exist, the injector makes one using the registered provider, then adds it to the injector before returning it. If an instance of the service already exists in the injector, that instance is returned. The component is then initialized using the returned service instance from the injector.
We've come far enough that we now need to run the app and see that the code we added works. Open the command line and run ng serve -o
. This should start the application and open it in the browser.
In this article, you learned about dependency injection as a pattern and how to use it in Angular. We walked through an example by creating a service and having the component's class know how to create that service. Then I introduced you to dependency injection, which is one of the ways Angular can make your application modular, extensible, and loosely coupled. With it, you make your component's focus on the view and how to render data. We moved code that knows how to retrieve data and manipulate the data away from the component's logic, into a service, then used dependency injection to allow Angular to pass that service into the component. With this approach, we achieved separation of concern where:
In the next article, you'll learn how to make HTTP requests in Angular. Stay tuned!😉
The code for this article can be downloaded from GitHub. It's contained in the src-part-4
folder. If you have any questions, feel free to leave a comment or reach out to me on Twitter.
Peter is a software consultant, technical trainer and OSS contributor/maintainer with excellent interpersonal and motivational abilities to develop collaborative relationships among high-functioning teams. He focuses on cloud-native architectures, serverless, continuous deployment/delivery, and developer experience. You can follow him on Twitter.