DEV Community

Cover image for Angular Techniques: Injecting Singleton Services into Custom Classes
Boris Jenicek
Boris Jenicek

Posted on • Edited on

Angular Techniques: Injecting Singleton Services into Custom Classes

How Angular's Dependency Injection Falls Short for Non-Service Classes

In Angular, dependency injection is primarily designed for services and components, facilitated through decorators like @Injectable() and @Component(). These decorators enable Angular to manage the creation and lifecycle of these objects. However, when you step outside this conventional use—into the realm of custom classes that are neither components nor services—you encounter a limitation. Angular's DI can't directly inject dependencies into arbitrary classes. This is where the Service Locator pattern comes into play as a workaround.

Setting Up the Angular Service Locator

The Service Locator pattern is essentially a manually created service that can access Angular’s injector itself. It acts as a central registry from which other parts of your application can retrieve dependencies. Here’s how to set it up:

  1. Define the Service Locator Class:
   import { Injector } from '@angular/core';

   export class ServiceLocator {
     static injector: Injector;
   }
Enter fullscreen mode Exit fullscreen mode
  1. Initialize the Service Locator: Create a function to assign the Angular Injector instance to your ServiceLocator:
   export function setupServiceLocator(injector: Injector): void {
     ServiceLocator.injector = injector;
   }
Enter fullscreen mode Exit fullscreen mode
  1. Register a Provider for Initialization: To ensure that the Injector is available as soon as possible, you need to register an APP_INITIALIZER provider in your application module:
   import { ApplicationConfig, APP_INITIALIZER, Injector } from '@angular/core';

export const appConfig: ApplicationConfig = {
     providers: [
       {
         provide: APP_INITIALIZER,
         useFactory: (injector: Injector) => () => setupServiceLocator(injector),
         deps: [Injector],
         multi: true
       }
     ]
   }
Enter fullscreen mode Exit fullscreen mode

Example: Using Angular Services in a Custom Class

With the Service Locator set up, you can now inject Angular services into any class. Here’s a simple example demonstrating how to use this approach:

import { ServiceLocator } from './service-locator';
import { LoggerService } from './logger.service'; 

export class CustomClass {
  private logger: LoggerService;

  constructor() {
    this.logger = ServiceLocator.injector.get(LoggerService);
    this.logger.log('CustomClass initialized!');
  }
}
Enter fullscreen mode Exit fullscreen mode

Why Use the Service Locator Pattern?

While this approach deviates from the typical Angular DI pattern, it provides a practical solution for extending dependency injection capabilities to any class within your Angular application. It's especially useful in large applications where you need to maintain clean architecture without excessively coupling your classes to Angular's components or services structure.


Simplify Angular DI with @ng-vibe/service-locator

If you're a fan of simpler, more streamlined approaches to managing dependency injection in Angular, you might be interested in a library I developed called @ng-vibe/service-locator. This library is designed to address the limitations of Angular's dependency injection for non-service classes, providing an elegant solution that supports both accessing singleton services and creating new instances dynamically.

Features of @ng-vibe/service-locator include:

  • Static Access to Services: Easily retrieve instances of any registered service from anywhere in your application.
  • Dynamic Service Creation: Create new service instances on-the-fly, scoped within their own injectors.
  • Simplified Service Management: Streamlines the use of Angular's injector, making it more accessible and easier to manage across your application.

Getting Started

To integrate @ng-vibe/service-locator into your Angular application, simply install the package via npm:

npm install @ng-vibe/service-locator
Enter fullscreen mode Exit fullscreen mode

Then, configure it in your application module:

import { provideNgVibeServiceLocator } from '@ng-vibe/service-locator';

export const appConfig: ApplicationConfig = {
  providers: [
    ...,
    provideNgVibeServiceLocator(),
  ],
}

Enter fullscreen mode Exit fullscreen mode

Example Usage

Here’s a quick look at how you might use @ng-vibe/service-locator to manage service instances effectively:

import { ServiceLocator } from '@ng-vibe/service-locator';
import { LoggerDummyService } from '../services/logger-dummy.service';

export class CustomClass {

  constructor() {
    const loggerSingleton = ServiceLocator.getInstance(LoggerDummyService);
    const loggerNewInstance = ServiceLocator.createService(LoggerDummyService);
  }
}
Enter fullscreen mode Exit fullscreen mode

Wrap-Up

The @ng-vibe/service-locator library makes extending Angular's DI capabilities straightforward and intuitive, especially useful in large applications where maintaining clean architecture is crucial. It helps you manage dependencies without tightly coupling your classes to Angular's components or services structure. For more details and how to contribute, check out the project on npm.

Stay Connected

For updates and more insights, follow me on LinkedIn.

Project Repository: https://github.com/boris-jenicek/ng-vibe

Top comments (0)