Getting instance of service without constructor injection
Asked Answered
A

7

142

I have a @Injectable service defined in Bootstrap. I want to get the instance of the service without using constructor injection. I tried using ReflectiveInjector.resolveAndCreate but that seem to create a new instance.

The reason I'm trying to do this is I have a base component derived from many components. Now I need to access a service but I don't want to add it to the constructor because I don't want to inject the service on all of the derivative components.

TLDR: I need a ServiceLocator.GetInstance<T>()

UPDATE: Updated code for RC5+: Storing injector instance for use in components

Avalos answered 27/5, 2016 at 11:34 Comment(0)
L
102

Yes, ReflectiveInjector.resolveAndCreate() creates a new and unconnected injector instance.

You can inject Angulars Injector instance and get the desired instance from it using

constructor(private injector:Injector) {
  injector.get(MyService);
}

You also can store the Injector in some global variable and than use this injector instance to acquire provided instances for example like explained in https://github.com/angular/angular/issues/4112#issuecomment-153811572

Linkboy answered 27/5, 2016 at 11:42 Comment(17)
Storing injector in a global variable in bootstrap.then() sound like what I need.Avalos
The question says without a constructor which makes this answer rather unhelpful.Briony
Without constructor there is no injection. Should we delete the question then ;-)Distinctive
@GünterZöchbauer, do you know which injector instance is injected when used like this private injector:Injector? Is it root injector instance? Or it depends?Jameson
If the constructor is in a component, it's the components injector, in a service it's the module injector (basically root injector if it's not a lazy loaded module). You can also get the parent components injector by adding @SkipSelf(). You can also interate the parent getter, to get the root injector.Distinctive
@GünterZöchbauer, got it, thanks! it's the components injector - even if it's not defined any providers?Jameson
Yes, a component always has some providers, like ElementRef registered. There is no component or directive without an injector. An injector always provides its own providers and all providers of parent injectors - just as a note.Distinctive
I've never seen mentioning that a directive has it's own injector. Do you know where can I read about that? And is my understanding correct that private injector:Injector is an instance returned by ReflectiveInjector.resolveAndCreate?Jameson
I don't know about docs, but you can register providers on a directive the same as a component. A component is basically just a directive with a view. If a directive is added to a component they share providers (don't know how this is exactly implemented). Yes, I'm pretty sure a components injector is built this way.Distinctive
@GünterZöchbauer It didn;t work for me with opaqueTokens. I've Already asked here . I'm hoping to find a solutionBetweentimes
This just puts something else in the constructor. How do you get it without a constructor.Aviation
@Aviation magic? You can share the injector in a static field or global variable and access it from there, but first you need to inject it "somewhere" using a constructor and assign it to the static field.Distinctive
Is the DI container ng uses that bad, that it doesn't even support the basic features of a common DI container?Aviation
@GünterZöchbauer That isn't an answer, that is an excuse.Aviation
Why would I want to spend my time for someone who downvotes my answer just becuase he wants something else?Distinctive
Hi, injector.get(ViewContainerRef) is returning null in my case. Could you please reply me with the possible situations if any to return null.Wainscot
Thank you. I`m using this variant, too. This is not an exact answer to the question because Injections needs to be passed by constructor. But it is much better than adding every needed service to the base class and then have to change all classes deriving it if I need another service. So this is a great answer.Skillern
B
127

In the updated Angular where ngModules are used, you can create a variable available anywhere in the code:

Add this code in app.module.ts

import { Injector, NgModule } from '@angular/core';

export let AppInjector: Injector;
    
export class AppModule {
  constructor(private injector: Injector) {
    AppInjector = this.injector;
  }
}

Now, you can use the AppInjector to find any service in anywhere of your code.

import { AppInjector } from '../app.module';

const myService = AppInjector.get(MyService);
Beckham answered 14/3, 2017 at 12:32 Comment(9)
Nice. Might be a good answer at #39409828 as well.Domenicadomenico
You're the king!Stirring
Wow are we allowed to do this ?Saltern
This has a small problem. You have to make separate injectors for lazy loaded modules that provide a service.Airfield
Yeah 🥳 lets make everything just a global public static monolithic variable. NOT 🤨Impersonate
@BernoulliIT I'd argue this makes sense when used in an abstract class - changing the abstract class injectors will not require changing constructors of every single inheritor. This is in line with the open-closed principle.Standfast
I don't see this AppModule being an abstract class 👀 and furthermore do you mean constructors instead of injectors? If this is so your statement is not true (at least not in Angular) and derived classes do have to provide constructor arguments to be passed through to the super/base class constructor, also when this is an abstract class.Impersonate
@Youp bernoulli We have a use-case where an abstract class injects services using this pattern, exactly so superclasses do no need to declare and inject all services with super(). We don't like the existence of the use-case, but it does make it more bearableFlatten
It might make it more bearable. Personally I am not afraid or lazy to refactor tens or even hundreds of components/services/classes to pass a service instance to a base class. Sometimes you have to roll up your sleeves 💪🏼 for quality and consistency of architecture/code. How much time can it take? 8 hours with some intelligent CTRL+SHIFT+H 😉Impersonate
L
102

Yes, ReflectiveInjector.resolveAndCreate() creates a new and unconnected injector instance.

You can inject Angulars Injector instance and get the desired instance from it using

constructor(private injector:Injector) {
  injector.get(MyService);
}

You also can store the Injector in some global variable and than use this injector instance to acquire provided instances for example like explained in https://github.com/angular/angular/issues/4112#issuecomment-153811572

Linkboy answered 27/5, 2016 at 11:42 Comment(17)
Storing injector in a global variable in bootstrap.then() sound like what I need.Avalos
The question says without a constructor which makes this answer rather unhelpful.Briony
Without constructor there is no injection. Should we delete the question then ;-)Distinctive
@GünterZöchbauer, do you know which injector instance is injected when used like this private injector:Injector? Is it root injector instance? Or it depends?Jameson
If the constructor is in a component, it's the components injector, in a service it's the module injector (basically root injector if it's not a lazy loaded module). You can also get the parent components injector by adding @SkipSelf(). You can also interate the parent getter, to get the root injector.Distinctive
@GünterZöchbauer, got it, thanks! it's the components injector - even if it's not defined any providers?Jameson
Yes, a component always has some providers, like ElementRef registered. There is no component or directive without an injector. An injector always provides its own providers and all providers of parent injectors - just as a note.Distinctive
I've never seen mentioning that a directive has it's own injector. Do you know where can I read about that? And is my understanding correct that private injector:Injector is an instance returned by ReflectiveInjector.resolveAndCreate?Jameson
I don't know about docs, but you can register providers on a directive the same as a component. A component is basically just a directive with a view. If a directive is added to a component they share providers (don't know how this is exactly implemented). Yes, I'm pretty sure a components injector is built this way.Distinctive
@GünterZöchbauer It didn;t work for me with opaqueTokens. I've Already asked here . I'm hoping to find a solutionBetweentimes
This just puts something else in the constructor. How do you get it without a constructor.Aviation
@Aviation magic? You can share the injector in a static field or global variable and access it from there, but first you need to inject it "somewhere" using a constructor and assign it to the static field.Distinctive
Is the DI container ng uses that bad, that it doesn't even support the basic features of a common DI container?Aviation
@GünterZöchbauer That isn't an answer, that is an excuse.Aviation
Why would I want to spend my time for someone who downvotes my answer just becuase he wants something else?Distinctive
Hi, injector.get(ViewContainerRef) is returning null in my case. Could you please reply me with the possible situations if any to return null.Wainscot
Thank you. I`m using this variant, too. This is not an exact answer to the question because Injections needs to be passed by constructor. But it is much better than adding every needed service to the base class and then have to change all classes deriving it if I need another service. So this is a great answer.Skillern
L
7

Another approach would consist of defining a custom decorator (a CustomInjectable to set the metadata for dependency injection:

export function CustomComponent(annotation: any) {
  return function (target: Function) {

    // DI configuration
    var parentTarget = Object.getPrototypeOf(target.prototype).constructor;
    var parentAnnotations = Reflect.getMetadata('design:paramtypes', parentTarget);

    Reflect.defineMetadata('design:paramtypes', parentAnnotations, target);

    // Component annotations / metadata
    var annotations = Reflect.getOwnMetadata('annotations', target);
    annotations = annotations || [];
    annotations.push(annotation);
    Reflect.defineMetadata('annotations', annotations, target);
  }
}

It will leverage the metadata from the parent constructor instead of its own ones. You can use it on the child class:

@Injectable()
export class SomeService {
  constructor(protected http:Http) {
  }
}

@Component()
export class BaseComponent {
  constructor(private service:SomeService) {
  }
}

@CustomComponent({
  (...)
})
export class TestComponent extends BaseComponent {
  constructor() {
    super(arguments);
  }

  test() {
    console.log('http = '+this.http);
  }
}

See this question for more details:

Lungworm answered 27/5, 2016 at 12:12 Comment(2)
The problem is tsc doesn't let me call super(arguments) because the arguments for the base constructor don't match.Heliogravure
in that particular case we can just remove constructor from derived class github.com/Microsoft/TypeScript/issues/12439Remanence
E
6

Angular 14 introduced an inject function.

According to the documentation

In practice the inject() calls are allowed in a constructor, a constructor parameter and a field initializer:

@Injectable({providedIn: 'root'})
export class Car {
  radio: Radio|undefined;
  // OK: field initializer
  spareTyre = inject(Tyre);

  constructor() {
    // OK: constructor body
    this.radio = inject(Radio);
  }
}

And

It is also legal to call inject from a provider's factory:

providers: [
  {provide: Car, useFactory: () => {
    // OK: a class factory
    const engine = inject(Engine);
    return new Car(engine);
  }}
]

In your case you can therefore use

class MyClass {
serviceLocator = inject(ServiceLocator)
}
Electrotechnology answered 1/6, 2023 at 6:1 Comment(0)
K
0

After running into this issue a few times, I've devised a good way to overcome it by using a getter with the Angular Injector service, instead directly injecting the service in the constructor. This allows the service time to be constructed before being referenced. My example uses only services but the same thing can be applied to a component using a service, just put the getter in a component instead BService in the example.

What I did was use a getter to inject the service into a class property using the Injector class, if the class property was not already set before, so the service is only ever injected once (the first time the getter is called). This allows the service to be used in basically the same way as if it was injected in the constructor but without a circular reference error. Just use the getter this.aService. They only time this won't work is if you are trying to use AService within the constructor of Bservice, then you would have the same issue of a circular reference since Aservice would not be ready yet. By using the getter you are deferring injecting the service until you need it.

There are arguments that, AService depending on BService, and BService depending on AService, is bad form but there exceptions to every rule and every situation is different so this is an easy and effective way to deal with this issue in my opinion.

// a.service.ts
import { Injectable } from '@angular/core';

import { BService } from './b.service';

@Injectable({
  providedIn: 'root'
})
export class AService {

  constructor(
    private bService: BService,
  ) { }

  public foo() {
    console.log('foo function in AService!');
    this.bService.bar();
  }
}
// b.service.ts
import { Injectable, Injector } from '@angular/core';

import { AService } from './a.service';


@Injectable({
  providedIn: 'root'
})
export class BService {
  // Use the getter 'aService' to use 'AService', not this variable.
  private _aService: AService;

  constructor(
    private _injector: Injector,
  ) { }

  // Use this getter to use 'AService' NOT the _aService variable.
  get aService(): AService {
    if (!this._aService) {
      this._aService = this._injector.get(AService);
    }
    return this._aService;
  }

  public bar() {
    console.log('bar function in BService!');
    this.aService.foo();
  }
}
Kneedeep answered 22/2, 2021 at 12:29 Comment(0)
D
0

I was facing a bizarre scenario using auth service and jwt interceptor. I was reading the user account info from locally stored storage. If the info is null, then get a fresh copy of account info. The interceptor was creating an empty object which was even not instance of AuthService.

this.authService instanceOf AuthService // false

The reason was, While creating the instance of AuthService, There was a request sent to server even before completion of execution of constructor of AuthService and the service was injected into the interceptor.

I resolved it by delaying the info call by 500 ms as it was not a high priority data and then the app is working fine.

This may not answer your question, but this may answer the ones searching with similar keywords.

Decline answered 1/2 at 7:33 Comment(0)
P
-3

StoreService .ts

  import { Injectable} from '@angular/core';
    
    @Injectable()
    export class StoreService {
      static isCreating: boolean = false;
      static instance: StoreService ;
    
      static getInstance() {
        if (StoreService.instance == null) {
          StoreService.isCreating = true;
          StoreService.instance = new StoreService ();
          StoreService.isCreating = false;
        }
        return StoreService.instance;
      }
      constructor() {
        if (!StoreService.isCreating) {
          throw new Error('You can\'t call new in Singleton instances! Call StoreService.getInstance() instead.');
        }
     }
    
  MyAlertMethod(){
    alert('hi);
    }
  }

.ts

//call this service directly in .ts as below:-

 StoreService.getInstance().MyAlertMethod();
Project answered 28/4, 2020 at 7:36 Comment(1)
How can this work with services that requires other services injected in the constructor?Niobe

© 2022 - 2024 — McMap. All rights reserved.