Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Scoped Services (Injectables) #23770

Closed
lucasbasquerotto opened this issue May 8, 2018 · 6 comments
Closed

[Feature] Scoped Services (Injectables) #23770

lucasbasquerotto opened this issue May 8, 2018 · 6 comments

Comments

@lucasbasquerotto
Copy link

lucasbasquerotto commented May 8, 2018

I'm submitting a...

[x] Feature request
Angular version: 5+

Hi!

I would like to ask for a feature to allow services (@Injectable) scoped to a component/(other) service/directive/etc that have it injected.

Situation

I consider the best approach to create one module per component, this way I need only to reference direct dependencies (because the indirect ones will be imported by the submodules).

This makes maintenance much more easier when I change a component from a feature module to another (I will remove only the component module from the feature module, and the dependencies are already handled).

This is also better for performance/app size reasons, because each lazy loaded module will have only what its needs.

I can use this approach for components, directives and pipes.

The problem is that I can’t use it for services/providers, because Angular behaves differently and it might cause problems if I forget to import it in one dependent module, but a parent component imports it, giving the impression that everything is alright (that is, it might work in some cases and don’t work in others, making maintenance much harder as the app grows, and the worse is that the errors are runtime errors that may or may not happen depending if a parent component imports the service).

It's important to note that what I'm asking is not the same as injecting in the providers array of the component, because I still need to import indirect dependencies in this case, having the same problems as above.

Also, all services in a module are created when the module is loaded and stay alive during the lifetime of the application.

Based on almost every case I see in the internet, people import all (or almost all) services in AppModule so that the services are created in the startup and are singletons. It becames even worse in the case of third party libraries (having to import all their services in AppModule so that I can use it, even if I use only one service, and that service is only used in lazy loaded modules).

I don't blame them, though. In my own project I do that to avoid the problems above (maintenance nightmare). It has 60 services and their size is not insignificant.

To have a perfomant app I would need to lazy load the ones I don't need in the initial load, and the best approach would be the same one that I use with components, referencing only direct dependencies, so that when I change one service A that injects some service B to now inject service C and not inject service B anymore, than I would change only ServiceAModule to import ServiceCModule and remove ServiceBModule from it, and all components that injects service A would still be working fine.

Proposal

I hope the Angular team creates a feature to make providers module scoped (without being seen in modules that do not import them directly), just like components, otherwise I see the way that is now as an impediment to create medium and large performant apps and at the same time easy to maintain.

For backwards compatibility, it could be some flag in the @Injectable annotation, like @Injectable({ scoped: true }), or a new annotation like @Scoped(), that would be like @Injectable(), but it would be scoped only to the module that imports it directly.

Example

  1. Component CA injects services SA, SB and SC
  2. Service SB injects service SD
  3. Service SC injects service SA

Then CA will be created like:

1. CA will be created
2. CA needs SA
2.1. SA will be created
2.2. SA#1 was created
3. CA needs SB
3.1. SB will be created
3.2. SB needs SD
3.2.1. SD will be created
3.2.2. SD#1 was created
3.3. SB#1 was created
4. CA needs SC
4.1. SC will be created
4.2. SC needs SA
4.2.1. SA will be created
4.2.2. SA#2 was created
4.3. SC#1 was created
5. CA#1 was created

The destruction would be:

1. CA#1 was destroyed
1.1. SA#1 was destroyed
1.2. SB#1 was destroyed
1.2.1. SD#1 was destroyed
1.3. SC#1 was destroyed
1.3.1. SA#2 was destroyed

(With SA#1 and SA#2 being different instances of the same type of service (SA))

Their modules would be:

  1. CAModule declares and exports CA and imports SAModule, SBModule and SCModule.
  2. SAModule declares and exports SA.
  3. SBModule declares and exports SB and imports SDModule.
  4. SCModule declares and exports SC and imports SAModule.
  5. SDModule declares and exports SD.

(If CAModule doesn't import SAModule it will give an error always, even if it imports SCModule and SCModule imports SAModule, avoiding the maintenance problems I talked about previously)

Pros

1) Much better performance (compared to import all of them in AppModule)

2) Much easier maintenance (compared to import the services in feature modules, or in the component providers array)

Cons

1) To use services to behave as singletons I would make one, and only one service, to be global, and inject this service to preserve the shared state of services of the same type, and listen to events of the service . I would just need to create a map (each key belonging to a different type of service) to have the (serializable) object with the state and a rxjs subject to emit and subscribe to events (could be a map of subjects, similar to the map with the states). This is just an idea and I could create it myself, my point is that I could achieve services to share state using the approach above. Although this can be achieved, this would require some effort, and the services should be changed accordingly, but nothing that can't be done.

2) Possible performance problems after the app is loaded, because it would be created one new service instance in each injection (but only when needed, and destroyed when not needed anymore, so in some cases it could actually improve performance).

3) I need to import all direct dependencies modules in each component/service that needs them.

More details

More information of why I consider one module per component the best approach:

https://stackoverflow.com/questions/46434830/should-i-create-a-module-per-component-in-angular-4-app/49902078#49902078

I can still use feature modules, but they would import the component modules instead of the components themselves (so, no problems with indirect dependencies).

The reason that makes me want one module per service is the same, but I can't do that because Angular doesn't support it :/

TL;DR

Because of the way services are currently imported in modules (inside the providers array), scoped to that module and all child modules, I need to import all of them in AppModule to avoid indirect dependencies that need to be imported explicitly and errors that don't happen in every case depeding on the dependencies of a parent module (maintenance nightmare).

The problem is that as the app grows, it will make the initial load size huge and the startup slower (bad UX).

So it would be really useful a scoped injectable that is created when the component/service that injects it is created and destroyed when the component/service is destroyed.

It would need to be imported explicitly in the module that imports the component/service (would not work even if a parent imported it, just like what happens with components/directives/pipes), to avoid cases where the same service works in some cases and not in other cases because of its parents dependencies (this is one of the worst kind of problems there is in software/web/app development IMO, and it is what happens now).

For medium and large apps, I don't see a way to make Angular apps both performant and easy to maintain, because of the reasons above.

Can this feature be implemented? Is there another way to achieve both performance and easy maintenace for medium and large sized Angular apps currently (without fighting against the framework and having in mind the problems I talked about)?

@mlc-mlapis
Copy link
Contributor

mlc-mlapis commented May 8, 2018

@lucasbasquerotto ... did you think already about new providedIn: 'root' metadata on a service level in Angular 6? Its main sense is to keep services declaration in lazy loaded modules but instantiate them on the main app module level as singletons when loading any of that module.

@lucasbasquerotto
Copy link
Author

@mlc-mlapis I had seen some features of Angular 6 before, hadn't seen that tough.

Now I read about it in:
https://github.com/angular/angular/blob/master/aio/content/guide/dependency-injection.md#tree-shakable-providers
and
https://blog.ninja-squad.com/2018/05/04/what-is-new-angular-6/#tree-shakeable-providers

If I understood correctly, if the lazy loaded modules LazyLoadedModuleA and LazyLoadedModuleB have the components ComponentA and ComponentB, respectively, and both components use (inject) the service ServiceA, and ServiceA has providedIn: 'root', then:

  1. In the build, the service code will be in the modules that have components/services that use it (if only lazy loaded modules use it, it won't be included in the initial load).
  2. LazyLoadedModuleA is loaded.
  3. ServiceA can be injected by the root injector, so ComponentA can use it.
  4. LazyLoadedModuleB is loaded.
  5. ServiceA is already loaded, so ComponentB can use it, and it would be the same instance than the one injected in ComponentA.

All of this without having to import it in any module.

Is that it? If it is, than that's awesome :)

It will work using injector too? (without including in the constructor)

I think I will make some test project when I have more time to see the behaviour (I still can't update my current project to Angular 6 due to some dependencies (Ionic) that require Angular 5).

@mlc-mlapis
Copy link
Contributor

mlc-mlapis commented May 8, 2018

@lucasbasquerotto ... you described it exactly. Using Injector ... yes, it should because the main point is that the service is instantiated under the root injector. The rest is as it was.

@wKoza
Copy link
Contributor

wKoza commented May 8, 2018

It will work using injector too? (without including in the constructor)

Yes

@mhevery
Copy link
Contributor

mhevery commented May 8, 2018

already implemented...

@angular-automatic-lock-bot
Copy link

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.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 13, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants