Skip to content

[Proposal] Defining non-singleton services or component's service #18015

Closed

Description

@ghetolay

Context

So I found a nice way to compose my Directives/Components. I use a service that's not part of any module but just defined at Directive/Component level.

Here is an example :

@Component({
  providers: [ NgrxService ]
})
export class MyComponent {
  constructor( @Self() private ngrx: NgrxService) { }
}

So NgrxService is not meant to be a singleton service but rather a way to compose my Component with reusable code. The service look like this :

@Injectable()
export class NgrxService {
  constructor(private store: Store, private cdr: ChangeDetectionRef) {
  }

  select() { ... }
  dispatch() { ... }
  set() { ... }

  ngOnDestroy(){
    ....
  }
}

I'm also talking about that kind of service here #17948.

This is really handy because you can inject Directive/Component related stuff like ViewContainerRef, ChangeDetectorRef... and you have even access to ngOnDestroy life cycle hook.
So it's wonderful and everything work great but....

Problem

The catch is that you have to always provide the service for each component, if you happen to forget it you'll probably end up with a parent service which you absolutely don't want. You can use @Self() to protect from it but well if you forget to provide it you'll probably also forget to use @Self().
Even if you have a really good memory it's still boilerplate and it feels like we're doing the job at the wrong end. The service is meant to work like that so that behavior should be determined during the service definition and not during its usage.

Proposals

There is probably multiple ways of achieving this but here is my proposal :
Create a new decorator for that kind of service:

  1. Those service won't be singleton but a new instance will be created for each injection.
  2. Can't be injected elsewhere than inside Component or Directive and get the same life cycle as the host Component / Directive.
  3. Must be provided by the component/directive's module or part of an imported module (do we need to export it ?). Their injection is scoped to the module and not global (like Component, Directive and Pipe).

Update

Since v6, this proposal is now about adding a component value to providedIn.

Activity

  1. mlc-mlapis commented on Jul 8, 2017

    @mlc-mlapis
    Contributor

    Yes, good idea. Probably this pattern is used already now but not as the first-order solution but rather at random according to a specific situation.

    Some points:

    • maybe just a parameter of @Injectable() will be enough and not a new decorator,
    • if all other hooks will be available then what should be the order in relation to component's hooks,
    • actually it is not possible to export providers from modules.
  2. ghetolay commented on Jul 8, 2017

    @ghetolay
    ContributorAuthor
    • At first I thought about a singleton property (boolean) but due to their particularity I think a new decorator would be better because they shouldn't even be called service IMO. We need to come up with a new name.
    • Only ngOnDestroy hook is available on services, this part is not from the proposal it's how it works at the moment.
    • I know we can't export services but I'm wondering if we should with those special services, like for Component, Directive and Pipe as they're much closer to that kind of injection rather than services.

    Maybe I shouldn't have talked about service, I really don't see them as service but just as a class than can be injected. They're like partial component.

  3. mlc-mlapis commented on Jul 8, 2017

    @mlc-mlapis
    Contributor

    They look like helpers but it is probably too general name.

  4. trotyl commented on Jul 9, 2017

    @trotyl
    Contributor

    I guess the original use case can be easily solved by the Directive as a Service pattern.

    Say that, your Component is configured with some selector:

    @Component({
      selector: 'my-component',
      template: '',
    })
    class MyComponent { }

    Then made a Directive with the same selector (or several different ones if needed):

    @Directive({
      selector: 'my-component'
    })
    class MyDirective { }

    Make the Directive to implement your Service:

    @Directive({
      selector: 'my-component'
    })
    class MyDirective implements MyService {
      myMethod() { }
    }

    Provide the MyService token with itself in the Directive:

    @Directive({
      selector: 'my-component',
      providers: [
        { provide: MyService, useExisting: forwardRef(() => MyDirective) }
      ],
    })
    class MyDirective implements MyService {
      myMethod() { }
    }

    Inject the Service in your Component (here make sure it's provided by companion Directive rather than parent):

    @Component({
      selector: 'my-component',
      template: '',
    })
    class MyComponent {
      constructor(@Host() private myService: MyService) { }
    }

    It solves:

    • You don't need to provide the Service each time, the Directives would be created automatically by Angular;
    • It's OK to inject view-specific dependency (like ChangeDetectionRef) in @Directive(), but not always OK in @Injectable();
    • Most life-cycle hooks are available in Directive, can be easily used.

    PS: The is how the NgModel directives works with all different kinds of inputs and custom components.

  5. ghetolay commented on Jul 9, 2017

    @ghetolay
    ContributorAuthor

    The aim of this proposal is to reduce some boilerplate and make this less error prone since it's currently working.

    With your suggestion I'll have to do a double link, for each component you have to inject the directive (dunno why you use a service token we can directly inject the directive) and add the component selector to the directive selector.

    It's less error-prone in the sense you can't end up injecting the wrong directive like you can with service. You'll either have the right directive or an error. But this makes the directive not really reusable as you need to edit it each time you want to use it on a component.

  6. trotyl commented on Jul 9, 2017

    @trotyl
    Contributor

    @ghetolay There may be several Directives implements the same Service, as a Component, you should not care about where it is from (even if you can), that's what called separation of concerns.

    It's less error-prone in the sense you can't end up injecting the wrong directive like you can with service. You'll either have the right directive or an error. But this makes the directive not really reusable as you need to edit it each time you want to use it on a component.

    And that's all being avoided this way.

    for each component you have to inject the directive

    For the Component you don't need to change anything except @Self() => @Host().

  7. trotyl commented on Jul 9, 2017

    @trotyl
    Contributor

    and add the component selector to the directive selector

    That's valid, maybe we can have a quick way to reuse the existing selector like:

    @Directive({
      with: MyComponent
    })
    class MyDirective { }

    To mark it as the same selector as MyComponent's.

  8. trotyl commented on Jul 9, 2017

    @trotyl
    Contributor

    Possibly relates to:

  9. ghetolay commented on Jul 9, 2017

    @ghetolay
    ContributorAuthor

    I really don't get why you would alias your directive and inject the alias rather than the directive directly, there is no interface, abstract class or multiple implementation, I'm just talking about a plain class doing stuff.
    Anyway the trick on your solution is all about using the same selector twice to spread the code logic.

    But again even we were to use with: MyComponent instead of selector we still need to reference all components using that directive. That's what I called the double reference, if C needs S, S is injected into C (we're good here) but we also needs to reference C into S (the selector). That's the killer.
    I prefer the way I'm doing it now even if I have to define the service inside providers because it's all inside C. We create S once and add it as a dependency everywhere it's needed (on C1, C2...) without having to update S so it can be injected on (C1, C2...).

    I think only #17017 is related, seems to be the same source problem.

  10. trotyl commented on Jul 10, 2017

    @trotyl
    Contributor

    I appreciate your proposals here, but that's not sound enough as far as I know.

    When saying Directive below, I mean the Directives other than Component.

    Create a new decorator for that kind of service

    Services never have a Decorator, is just a name for what being injected by DI.

    And Injector has no restriction about how you provide them, wherever you can use useClass, you can use useFactory or, but@Injectable() is just for the useClass.

    So even if you put more information in the Decorator, it won't help with other cases.

    Those service won't be singleton but a new instance will be created for each injection

    What should happen when more than one Directives in the same host injects this Service? Should they get two different instances?

    Can't be injected elsewhere than inside Component or Directive

    The Scope of a Service is determined by how it's provided, so for providers it would be available in this Component and its view children, for viewProviders it would only be available for its view children.

    So in this case you would need a new properties called selfProviders to provide those Service with Scope of just this Component. But then what about Directives? They don't have a way to declare any kind of providers.

    get the same life cycle as the host Component / Directive

    If a general life-cycle hooks sharing mechanism being introduced, then:

    • Should Directives be able to use life-cycle hooks of the Component in the same host?
    • Should Pipes be able to use life-cycle hooks of the Component or Directive?
    • Should Services be able to use life-cycle hooks of the Component or Directive?
    • Should Service has different life-cycle hooks when they're injected in Component and Directive?

    And since life-cycle hooks is only for useClass case, then what about the inconsistency between useClass and other cases? Should we introduce other mechanism to compensate that?

    Must be provided by the component/directive's module or part of an imported module

    This means a imported module can easily break your application all the time, if a module declare Store as not singleton, what should it be? An separate store for each Component? Then how can a module cancel the non-singleton declaration made by a dependency module?


    There have been many thoughts about DI if you could have a search, but most of them just stay on the usability for their own use case, but never consider how many problems it would cause.

    PS: The links above is for others who need to take action.

  11. ghetolay commented on Jul 10, 2017

    @ghetolay
    ContributorAuthor

    There is a big misunderstanding here. I'm not introducing any new mechanism it's something that's working right now. I'm just proposing a different way to trigger that mechanism.

    Exactly like @Injectable, just a more declarative way of what we're doing and less error-prone because it would be handled by the framework under the hood rather than the user.

    Take the 2 first snippets, I would like to remove providers: [ NgrxService ] and @Self() on the first snippet by adding something on the second snippet. Because I think the behavior induced by providers: [ NgrxService ] should be defined at service level. That service was created to work that way, always everywhere.
    My proposal being replacing @Injectable() with another decorator name but totally open for any suggestion like a new parameter on @Injectable().

    If this is rejected there'll probably be a way to do it using schematics and generate a third party pre-processing tool.

  12. trotyl commented on Jul 10, 2017

    @trotyl
    Contributor

    Can't be injected elsewhere than inside Component or Directive

    This would be a huge break of current DI mechanism, in the existing concept of DI, you can inject anything you want as long as the token can be found by the injector, it is why that's working right now for Service being able to inject View-specific stuff. (may not work in 2.x but that can be considered bug)

    So this is a completely new DI mechanism for injectors to not being hierarchical anymore, so it may need to take a lot cases into consideration.

    In short, back in old days (before the view engine), there're indeed some dependencies that can only be injected into Component, like ChangeDetectorRef, ElementRef, TemplateRef and ViewContainerRef (they're injected by compiler, not injector). But that's considered a bug due to the nature of DI and being fixed, after that your code can now work.

    Again, you propose that some dependency should just be able to use in Component. Then questions come: Why shouldn't other consider this a bug as well? Why should your code work in first place?

    Exactly like @Injectable

    Not exactly AFAIK, every other type is also @Injectable(), it's capable to inject @Component(), @Directive() or @Pipe() as long as it's provided.

    And again, Decorator here would only work for useClass, so whatever information added here would not fit the other ways, that's another break for current DI mechanism.

    @Injectable() just stands for @ThisIsAMeaninglessDecoratorButWeNeedThisDueToTypeScriptRestriction() at first then @ThisIsStillAMeaninglessDecoratorBugWeRequireItForConsistency(), what you proposed is not meaningless, so that's not something like @Injectable().

  13. added
    featureLabel used to distinguish feature request from other issues
    on Jul 24, 2017
  14. igabesz commented on Oct 4, 2017

    @igabesz

    Here is a question of mine on SO showing a possible case for usage. TLDR: I'd like to inject separate Logger instances into various components and services. This proposal would make the story a bit more convenient. We can solve it now but with a little more verbosity.
    https://stackoverflow.com/questions/46573239/

  15. 1 remaining item

  16. ghetolay commented on May 4, 2018

    @ghetolay
    ContributorAuthor

    With the new providedIn: 'root' in v6 it looks like a first step toward this feat request was made. Allowing us to define where a service gets provided as part of the service metas instead of inside modules/components providers.

    So this proposal is now about adding a new component value to use with providedIn.

    Also the most common use case would probably be an onDestroy service to use with takeUntil() when subscribing to observables inside components (until we get life cycle observables).

  17. trotyl commented on May 6, 2018

    @trotyl
    Contributor

    What you thought is far from possible.

    First of all, there's no concept of component in DI, only directive and view, what you think to be a component is just a normal directive on the root element of the specific view. So technically there can only be providedIn: 'directive' instead of providedIn: 'component'. But that's still wrong, the injection scope is not the directive, but its host element, so it's providedIn: 'element' instead.

    Even the API looks good now, what you want is the exact opposite behavior of providedIn. providedIn works as the fallback option, it would only be used when the token not already provided. But you want providedIn to be used first and even ignore the normal providers. Even though technically achievable, almost require redesign from beginning.

    Then how would you like to write unit tests for them? More clearly:

    • How to test the element-specific service?
    • How to mock the element-specific service and test the directive?

    "Let's just have integration tests then" is definitely not an option.

    And given those service can only be used by directives, why should Angular restrict the life-cycle hooks only to OnDestroy? Even you could say "I don't need" but others would still make the feature request as long as possible. So what's the difference between "element-specific service" and "dynamic directive on element"? Just same thing with different names?

  18. simeyla commented on Aug 14, 2018

    @simeyla

    To solve the 'forgetting to add @self' or 'forgetting to provide at the component level' problems I did the following. This was sufficient for me at this time to keep moving and protect myself from bad cut-and-pasting.

    • remove providedIn: 'root' from the service

    • created an init(this) method in my service that must be called immediately in the constructor of the component

    • the init() method checks to see if that service instance has ever been initialized before (suggesting you forgot to 'provide' it locally)

    • Methods on the service will fail if init() hasn't been called.

    Is tslint clever enough to be used to create a rule for any of this? Like a rule that says parameter-of-type-X-must-be-provided-by-decorator?

  19. bryce-marshall commented on Nov 21, 2018

    @bryce-marshall

    I would also find such a feature useful. I see it as being analogous to Angular pure and impure pipes. A hypothetical "impure" injection candidate would result in a new instance being created (or a factory method being invoked afresh) each time the candidate is injected into a constructor. Theoretically, such a provider could still be overridden at different levels of the injector hierarchy, in which case new instances of the overriding provider type would be created for each injection at or below their definitions. As @ghetolay has already argued, this would be expected behavior for such a service/factory.

    I my case, I want to take a state snapshot that is injected into components. I currently see no clean way of doing this within Angular.

    I have asked a Stack Overflow question explaining the problem here, however I suspect that the answer is that it can't currently be done:

  20. jsgoupil commented on Feb 16, 2019

    @jsgoupil

    I also had a question on StackOverflow that would get fixed with this feature request. The useFactory caches the data on a node. Which to me is quite problematic, I want the service to be always new when the constructor is being called.

    https://stackoverflow.com/questions/54726855/how-to-clear-the-provider-cache-to-use-with-usefactory-in-angular-7

  21. mlc-mlapis commented on Feb 16, 2019

    @mlc-mlapis
    Contributor

    @jsgoupil ... but there is already the correct answer on SO, so where is the problem?

  22. jsgoupil commented on Feb 17, 2019

    @jsgoupil

    @mlc-mlapis The "correct" answer is a workaround. Only took me a few hours to figure this out. There no explanation of cache for factory usage. This came to me as a surprise.

  23. pauldraper commented on Apr 1, 2019

    @pauldraper
    Contributor

    First of all, there's no concept of component in DI, only directive and view, what you think to be a component is just a normal directive on the root element of the specific view. So technically there can only be providedIn: 'directive' instead of providedIn: 'component'. But that's still wrong, the injection scope is not the directive, but its host element, so it's providedIn: 'element' instead.

    This sounds like a really long way of saying that you like the name 'element' over 'component'.

    Regardless of the name, the intention is the same: that we be able to inject additional things at the same level as ElementRef, ChangeDetectorRef, etc.

  24. trotyl commented on Apr 16, 2019

    @trotyl
    Contributor

    This sounds like a really long way of saying that you like the name 'element' over 'component'.

    @pauldraper I do agree this feature is more meaningful than I original thought, but this is not only about naming, but concept. Even element is still wrong name, but node is correct, a directive can be made on comment node without corresponding element, resulting a node-level injector.

    providedIn: 'component' is totally wrong as it's nothing special for component. providedIn: 'directive' is misleading as it won't create new instance for each directive, but still locally singleton service for that node injector (all directives in that node share the same service).

    I agree it's valuable even with selector-less directive landing simultaneously (with most functionality shared), a unique feature for providedIn: 'node' is the availability in child injectors, like:

    @Injectable({
      providedIn: 'node'
    })
    class MyService {}
    
    @Component()
    class MyComponent {
      constructor(injector: Injector) {
        const childInjector1 = Injector.create([], injector)
        const childInjector2 = Injector.create([], injector)
        childInjector1.get(MyService) === childInjector2.get(MyService) // true
      }
    }
  25. added
    feature: under considerationFeature request for which voting has completed and the request is now under consideration
    on Jun 4, 2021
  26. alxhub commented on Apr 14, 2022

    @alxhub
    Member

    Component DI is "closed" - the set of provided services are determined entirely when the component is compiled, and cannot be expanded at runtime. This is done for performance, as component creation requires a lot of injection operations.

    This means that we cannot support adding a provided service to a component after initialization, which would be necessary to implement providedIn: component.

  27. angular-automatic-lock-bot commented on May 15, 2022

    @angular-automatic-lock-bot

    This issue has been automatically locked due to inactivity.
    Please file a new issue if you are encountering a similar or related problem.

    Read more about our automatic conversation locking policy.

    This action has been performed automatically by a bot.

  28. locked and limited conversation to collaborators on May 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtimecore: difeatureLabel used to distinguish feature request from other issuesfeature: under considerationFeature request for which voting has completed and the request is now under consideration

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions