Communication between components using services – Angular Services and the Singleton Pattern
A characteristic that we must understand about Angular services is that, by default, every service instantiated by the dependency injection mechanism has the same reference; that is, a new object is not created, but reused.
This is because the dependency injection mechanism implements the singleton design pattern to create and deliver the objects. The singleton pattern is a design pattern of the creational type and allows the creation of objects whose access will be global in the system.
This characteristic is important for the service because, as the service deal with reusable business rules, we can use the same instance between components, without having to rebuild the entire object. In addition, we can take advantage of this characteristic and use services as an alternative for communication between components.
Let’s change our gym diary so that the ListEntriesComponent component receives the initial list by service instead of @Input:
export class ListEntriesComponent {
private exerciseSetsService = inject(ExerciseSetsService);
exerciseList = this.exerciseSetsService.getInitialList();
itemTrackBy(index: number, item: ExerciseSet) {
return item.id;
}
}
In the DiaryComponent component, we will remove the list from the input:
Server Sync
Running it again we can see that the list continues to appear. This is because the instance of the service used in both components is the same. However, this form of communication requires us to use RxJS to update the values with the buttons on the diary screen. We will go deeper into this topic in Chapter 9, Exploring Reactivity with RxJS.
We saw that, by default, the services are singleton, but in Angular, it is possible to change this configuration for another service if you need to solve some corner cases in your application.
When we create a service, it has an @Injectable decorator, as in our example:
@Injectable({
providedIn: ‘root’,
})
export class ExerciseSetsService {
The provideIn metadata determines the scope of the service. The value ‘root’ means that the instance of the service will be unique for every application; that’s why, by default, Angular services are singleton.
To change this behavior, let’s first return to the ListEntriesComponent component to receive @Input:
export class ListEntriesComponent {
@Input() exerciseList!: ExerciseSetList;
itemTrackBy(index: number, item: ExerciseSet) {
return item.id;
}
}
Let’s go back to inform the attribute in the DiaryComponent component:
Server Sync
In the ExerciseSetsService service, we will remove the provideIn metadata:
@Injectable()
export class ExerciseSetsService {
If we run our application now, the following error will occur:
ERROR Error: Uncaught (in promise): NullInjectorError: R3InjectorError(DiaryModule)[ExerciseSetsService -> ExerciseSetsService -> ExerciseSetsService -> ExerciseSetsService]: NullInjectorError: No provider for ExerciseSetsService!
This error happens when we inform Angular that the service should not be instantiated in the application scope. To resolve this error, let’s declare the use of the service directly in the DiaryComponent component:
@Component({
templateUrl: ‘./diary.component.html’,
styleUrls: [‘./diary.component.css’],
providers: [ExerciseSetsService],
})
export class DiaryComponent {
So, our system works again, and the component has its own instance of the service.
This technique, however, must be used in specific cases where the component must have its own instance of the services it uses; it is recommended to leave the provideIn in the services.
Let’s now start exploring our application’s communication with the backend using Angular.