4

Understanding Dependency Injection and Services in Angular

 1 month ago
source link: https://blog.bitsrc.io/dependency-injection-and-services-in-angular-7054e783a0b6
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
1*FUjO8NysuICHlK05k1DK_g.png

Building complex, modular Angular applications requires a solid understanding of dependency injection (DI) and services. These cornerstone features work hand-in-hand to deliver flexibility, maintainability, and testability to your Angular project.

So let’s take a look at these concepts in further and see how we can integrate them in our next Angular project.

Introduction To Dependency Injection in Angular

Dependency injection is one of the most fundamental concepts in Angular. Yes, you heard that right. Everything around Angular revolves around Dependency Injection.

Dependency Injection is wired into the Angular framework and allows classes with Angular decorators, such as Components, Directives, Pipes, and Injectables, to configure dependencies that they need.

By using DI with Angular, you are able to make your components:

  • More Modular: Components stay lightweight and focused, relying on injected dependencies instead of managing them directly.
  • Highly Testable: Easily mock and isolate dependencies for in-depth unit testing.
  • Highly Flexible: Change dependencies without modifying components, making code updates simpler.

Additionally, your Angular applications become robust, adaptable, and easier to test, ensuring long-term maintainability.

What Are The Core Concepts of Dependency Injection with Angular?

The core of dependency injection in Angular relies on two concepts:

  1. Services
  2. Providers
  3. Injection Tokens

Services

Think of services as your skilled workers tasked with specific jobs. They encapsulate reusable functionality and data, like:

  • Data access: Fetching and managing data from various sources (APIs, local storage).
  • Business logic: Implementing core application logic, independent of any specific component.
  • API communication: Handling API calls, data transformation, and error handling.
  • Singleton nature: Angular services are by default singletons. Only one instance is created per service throughout the application. However, this behavior can change based on the scope you’re injecting your service into.

All components injecting the same service receive the same instance, ensuring data consistency and efficient memory usage.

This means any changes made to data or state within a service are reflected across all components using it.

Pro Tip: While singletons are convenient, overuse can lead to tight coupling. Consider using provider scoping options like providedIn to create multiple instances when needed.

Providers

Providers are like specialized factories responsible for constructing and delivering the services your workers need. They tell the DI system where and how to create these services.

There are four providers in Angular, where each serves a specific purpose:

  • useClass : Creates a new instance of the specified class for each injection. Ideal for services in an independent state.
  • useValue : Injects a constant value directly. Useful for simple configuration values.
  • useExisting : Reuses an existing instance of another service. Avoids circular dependencies but is used cautiously.
  • useFactory : Provides a custom factory function for creating the dependency. Offers more flexibility for complex scenarios.
// Providing a DataService using useClass
@Injectable({
providedIn: 'root' // Singleton instance for the entire application
})
export class DataService {
// ... service logic
}

// Injecting DataService into a component
constructor(private dataService: DataService) {}

Injection Tokens

These act as unique identifiers for dependencies.

Imagine them as ID tags worn by your service workers, allowing DI to recognize and deliver them based on component requests. By doing so, they help DI distinguish between different service types, even if they share the same class.

How To Implement Dependency Injection and Services in Angular?

Now that you understand the core concepts, let’s see how we can implement DI and services in your Angular applications:

Creating and Registering Services

Manual Registration

One way to create a service in Angular is through manual registration. All you have to do is define providers directly in your module’s providers array, specifying the service class and provider options.

This is ideal for small-scale Angular apps with few services.

@NgModule({
providers: [
{ provide: DataService, useClass: DataService },
{ provide: LoggerService, useClass: LoggerService }
]
})
export class AppModule {}

Using @Injectable Decorator

Next, you can utilize the @Injectable decorator to define and build your service. All you have to do is decorate your service class with @Injectable , specifying provider options within the decorator itself.

This approach provides more flexibility and better code organization, while letting you customize the scope.

@Injectable({
providedIn: 'root' // Singleton instance for entire application
})
export class DataService {
// ... service logic
}

Pro Tip: Choose the approach that best suits your project’s needs and complexity. While manual registration is quick for simple cases, the @Injectable decorator offers more options and better maintainability for larger-scale projects.

Injecting Services Into Components

Once you have your services created and registered, you can inject them into your components to utilize their functionality. To do so, there are several approaches.

Constructor Injection

With this approach, you can utilize your component constructor to inject the necessary services.

All you have to do is define the services inside your constructor as input parameters and the Angular DI Service automatically injects the appropriate instances based on providers.

constructor(private dataService: DataService, private loggerService: LoggerService) {}

Property Injection

Next, you can use the @Inject decorator on properties to specify specific injection tokens or configurations. By doing so, you get more flexibility when you need finer control over injected instances.

@Inject(MyCustomToken)
myCustomService: MyCustomService;

Leveraging Service Scopes in Angular

As we discussed earlier, you can define scopes for your services to change its singleton behavior. For instance, you can define a service to be used through your app, component or a module.

Usage at Module

Use providedIn option in the @Injectable decorator to specify the module level. This is ideal for services shared across multiple components within a module.

@Injectable({
providedIn: 'SharedModule' // Service shared within SharedModule
})
export class SharedService {
// ... service logic
}

Component Level

Add providers directly to the component’s providers array. This is useful for services specific to a particular component or its child components.

@Component({
providers: [
{ provide: MyComponentService, useClass: MyComponentService }
]
})
export class MyComponent {}

Pro Tip: Consider provider scoping carefully. Providing services at the root level can lead to tight coupling, while overly specific scoping can increase boilerplate code. Aim for a balance that promotes both maintainability and efficiency.

Dependency Injection in Action with Angular

Let’s take a look at how we can build an application that leverages Dependency Injection with Angular. To do so, let’s build an app that renders a list of users when a button is clicked.

Our app would look something like this:

0*pS2tG3kXMn-HOeBY.gif

So, to get started, go ahead and create an Angular app. Next, let’s create the following:

  1. A User List Component: The user list will hold the UI for rendering the list of users.
  2. A Data Service: The Data Service will be responsible for fetching the list of users.
  3. A Logger Service: The Logger Service will be responsible for acting as a centralized logging service.

After you’ve created the following components and services, update each of these files with the code below:

user-list.component.html

<ul>
<li *ngFor="let user of users">
{{ user.name }}
</li>
</ul>

The User List component will render the the names of the users that it recieves.

user-list.component.ts

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

@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
styleUrls: ['./user-list.component.scss']
})
export class UserListComponent {
@Input() users: any[]= [];
}

As you can see, we’ve introduced a prop into the User List component to allow consumers to pass users that can be rendered in the template.

Next, let’s update the data service:

data.service.ts

import { Injectable } from '@angular/core';
import {HttpClient} from "@angular/common/http";

@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private http: HttpClient) {}

getUsers() {
return this.http.get('https://jsonplaceholder.typicode.com/users');
}
}

The data service will hold a method that will leverage the HTTP Module to fetch a list of users from an API.

Next, let’s add some logic to the Logger.

logger.service.ts

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

@Injectable({
providedIn: 'root'
})
export class LoggerService {
log(message: string) {
console.log(message);
}

}

The Logger Service will hold one method that will ideally log any message.

Finally, let’s bring this all together by calling all APIs in the App Component.

app.component.ts

import { Component } from '@angular/core';
import {LoggerService} from "./logger.service";
import {DataService} from "./data.service";

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'di-services-angular-demo';
dataFromService: any;

constructor(private dataService: DataService, private loggerService: LoggerService) {}

onClickGetUsers() {
this.dataService.getUsers().subscribe(users => {
this.dataFromService = users;
this.loggerService.log('Fetched user data successfully!');
});
}
}

app.component.html

<button (click)="onClickGetUsers()">Get Users</button>
<app-user-list *ngIf="dataFromService" [users]="dataFromService"></app-user-list>

After implementing the above demo app you should be getting a Get User button on your angular app.

Then after clicking on the button, you should see an output like the below screenshot.

0*2ldwWuEE559CyFoQ.png

Wrapping Up

And it’s as simple as that. It doesn’t take much to integrate dependency injection onto your Angular App.

If you want the entire code, checkout my GitHub repository.

I hope you found this article helpful.

Thank you for reading.

Learn More


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK