9

I have an Angular2 DI question. Say I have a TestService and I want to use 2 different instances of this service inside the same component. If I simply add a provider to the component and I add the 2 instances to the constructor I end up with the same service instance. For example:

TestService

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

@Injectable()
export class TestService {

    public id: number = Math.random();

    public toString(): string {
        return "Id: " + this.id;
    }
}

Test component

import {Component, Input, OnInit} from "@angular/core";
import {TestService} from "../../services/test.service";

@Component({
    providers: [TestService]
})
export class TestComponent implements OnInit {

    constructor(private _testService1: TestService, private _testService2: TestService) { };

    ngOnInit() {
        console.log("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", this._testService1.toString());
        console.log("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", this._testService2.toString());
    }
}

Result in console

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Id: 0.24242492129168425
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB Id: 0.24242492129168425

Can someone pls tell me if there's a way to use Angular2's DI mechanism to inject multiple different instances of a service within the same component or should I just drop the DI for this particular case and create my instances manually with a manual constructor?

Thanks in advance

RVP
  • 2,330
  • 4
  • 23
  • 34

3 Answers3

13

Given that there is finite amount of instances, the straightforward way may be:

@Component({
    providers: [
        { provide: 'TestService1', useClass: TestService },
        { provide: 'TestService2', useClass: TestService }
    ]
})
export class TestComponent implements OnInit {
    constructor(
        @Inject('TestService1') private _testService1: TestService,
        @Inject('TestService2') private _testService2: TestService
    ) {}
    ...
}

Or OpaqueToken counterpart to avoid overriding services with the same string identifier:

export const TestService1 = new OpaqueToken;
export const TestService2 = new OpaqueToken;

...
providers: [
    { provide: TestService1, useClass: TestService },
    { provide: TestService2, useClass: TestService }
]
...
constructor(
    @Inject(TestService1) private _testService1: TestService,
    @Inject(TestService2) private _testService2: TestService
) {}

It doesn't hurt DI in TestService constructor. And keeps the testability of TestComponent on par, both service instances can be mocked independently.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • 2
    Great answer. To complete, I would say OpaqueToken should also be used instead of the strings to avoid naming collisions: https://angular.io/docs/ts/latest/api/core/index/OpaqueToken-class.html – Charles HETIER Dec 06 '16 at 17:14
  • @CharlesHETIER Thanks, string identifiers were used here for brevity, but it doesn't hurt to mention that OpaqueToken can be used as well. – Estus Flask Mar 16 '17 at 12:25
6

You can inject a factory that returns a new instance every time you call it:

@NgModule({
   providers: [{
      provide: 'testService', 
      useFactory: (/* TestService deps here like `http`*/) => 
        (/* params */) => new TestService(/* http */), 
      deps: [/* TestService deps here like `Http`*/ ]
    }]
})


@Component(...)
export class TestComponent implements OnInit {

    constructor(@Inject('testService') private _testServiceFactory) { };

    ngOnInit() {
        console.log("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", this._testServiceFactory( /* params */).toString());
        console.log("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", this._testServiceFactory().toString());
    }
}

Plunker example (check the output in the browser console when you click the button)

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 2
    It's simply not doing the work. If you inject with useFactory, the factory will be used only once for injecting into _testServiceFactory. But this snippet will not going to work anyway, as you do not have type for the constructor argument, angular can't know what to inject. – Dmitriy Kachko Jan 23 '17 at 18:30
  • 1
    @TimKachko thanks a lot for your valuable feedback! I updated my answer. You are right, about the type. I changed it to use a string key instead with `@Inject()`. The factory will work though. Perhaps I could make it more obvious in the code, but the factory is a function that returns a function, therefore DI will keep an instance of the returned function and pass it to every class that injects `'testService'`, and because a function is passed to the constructor that returns a `new TestService()` every time it is called, this will work. – Günter Zöchbauer Jan 23 '17 at 18:36
  • Let you just try to get work it in the Plucker you'll see what I mean. the useFactory will inject TestService, not a factory method which you can use to create the TestService. – Dmitriy Kachko Jan 23 '17 at 18:54
  • Plunker doesn't work well with my Chrome version since a while :-/. Did you see the `useFactory: (...) => (...) => new TestService()` (double `() =>`) where the value returned by the factory is a factory function? – Günter Zöchbauer Jan 23 '17 at 19:01
  • Correct. This function engages by Angular automatically before construction of the TestComponent to inject the new TestService in the component. – Dmitriy Kachko Jan 23 '17 at 19:06
  • No, Di calls the factory and passes the return value to the constructor. The return value is again a function, therefore the inner function will be passed to the constructor and can be called there. – Günter Zöchbauer Jan 23 '17 at 19:09
  • https://angular.io/docs/ts/latest/guide/dependency-injection.html see example here – Dmitriy Kachko Jan 23 '17 at 19:12
  • 1
    it has only one `()=>` and therefore returns a class instance, while my example (double `()=>`)returns a function – Günter Zöchbauer Jan 23 '17 at 19:17
  • I added a Plunker. – Günter Zöchbauer Jan 24 '17 at 10:34
  • @GünterZöchbauer, what about memory leaks? Previous instances seem to be kept for the whole application life. – Daniel Kucal Feb 02 '17 at 12:49
  • No idea why you think that would be the case. If you don't hold a reference to the class yourself, the garbage collector will collect it. – Günter Zöchbauer Feb 02 '17 at 14:07
2

I would create a static method that returns new instance in the service, and inject it only one through DI in component. Something like:

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

@Injectable()
export class TestService {

    public id: number = Math.random();

    public toString(): string {
        return "Id: " + this.id;
    }
    static init() {
      return new TestService();
    }
}

then in component:

import {Component, Input, OnInit} from "@angular/core";
import {TestService} from "../../services/test.service";

@Component({
    providers: [TestService]
})
export class TestComponent implements OnInit {
    _testService1: TestService;
    _testService2: TestService;

    constructor(_testFactory: TestService) { 
       this._testService1 = _testFactory.init();
       this._testService2 = _testFactory.init();
    };

    ngOnInit() {
        console.log("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", this._testService1.toString());
        console.log("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", this._testService2.toString());
    }
}
Sasxa
  • 40,334
  • 16
  • 88
  • 102