angular / angular

Deliver web apps with confidence 🚀

Home Page:https://angular.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support OnInit in services

shlomiassaf opened this issue · comments

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report  
[X] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

Services has only the OnDestroy lifecycle hook. OnInit is only available in directives.

Expected behavior

OnInit should be available to services as well.

What is the motivation / use case for changing the behavior?

OnInit is a safe zone, where it is 100% safe to assume that all other services have be instantiated, including the instance of the module. (see #23058).

In the current state, injection order determines the ability to inject services, if service A and service B cross reference they will need an external event to be aware of each other.

My scenario is a service that requires the module instance, if the module instance also requires the service (so the svc is eager) we have a DI loop.

In my use case the service examines the interface of the module and do stuff based on it. To make it happen I need the module to manually call a method on the service... this is not a transparent process... the module should be independent here...

(yes, I know I can use providers, but in that case my approach fit's best for me)

CC @mhevery

Is there any progress or status on this?
It would be a nice way to keep instantiation of variables out of the constructor of a service?

Bump.

Seriously, this would be a great QoL addition to the framework, it would streamline cross-referenced services.

+1 for this one

+1
I'm surprised this isn't currently possible in Angular, since it's core to Spring (i.e. @PostConstruct).

+1
Since doing any other initialization besides field assignment is NOT RECOMMENDED in constructor, then having a support for an OnInit hook in services would be extremely helpful.

any other initialization besides field assignment is NOT RECOMMENDED in constructor

According to..?

According to..?

I think it's common sense. Your framework-injected dependencies are not available during object construction. Of course, you can always torture the code and DI framework to work around it, but as @lukaszbachman said, it's not recommended.

any other initialization besides field assignment is NOT RECOMMENDED in constructor

According to..?

According to Misko Havery himself: http://misko.hevery.com/code-reviewers-guide/

Your framework-injected dependencies are not available during object construction

I'm sorry, could you expand on this? Doesn't Angular/TS DI inject the actual service into the constructor ready to use..? How would accessing an instance's methods be "torturing the DI framework"?

According to Misko Havery

I wonder what Misko Havery would think about a framework-run OnInit method, triggered only by checking if the class implements a specific interface. :)

Was there any reason for not implementing this? Like any downsides or complication? Or it just doesn't fit for a service?

+1!

any other initialization besides field assignment is NOT RECOMMENDED in constructor

According to..?

Angular documentation: https://angular.io/tutorial/toh-pt4#call-it-in-ngoninit

@Blackbaud-PaulCrowder The documentation you link is about components, not services.

@Blackbaud-PaulCrowder The documentation you link is about components, not services.

It is, but the post to which I was replying was asking where there was guidance for not performing logic inside a constructor, so I gave an example in Angular documentation where this is stated. Not performing logic inside a constructor is general best-practice for object-oriented programming, regardless of the language or framework. The fact that Angular calls out this common practice in its component documentation is a pretty good argument for implementing it for services.

Sorry for replying to this ancient topic but my point is: Don't try to cite best practices in oop while simultaneously asking for a "give me an extra constructor-like thing that the framework will automatically run sometime, by checking the interface via reflection"

edit: Oh, sorry, @monocl-oskar is an old extra account of mine

My scenario is a service that requires the module instance, if the module instance also requires the service (so the svc is eager) we have a DI loop.

If you have circular dependency in the DI system, this will throw an error. Having onInit would not help with circular dependency problem. Could you provide more context.

onInit for Component is: "A lifecycle hook that is called after Angular has initialized all data-bound properties of a directive. Define an ngOnInit() method to handle any additional initialization tasks." (https://angular.io/api/core/OnInit)

So there is a clear reason for the onInit. What would such a thing be for a service? There is constructor, but there is no "later". I see there is a suggestion that "later" would be after all services are instantiated. But when the constructor runs all services are instantiated. So it would just be call constructor and than call onInit which does not seem useful. The DI circulare dependencies will throw an error as one can't inject circular dependencies, so onInit does not make much sense.

My point is that I don't see when such a method should be called. (other than right after construction, which has minimal benefits)

... What would such a thing be for a service?

One of examples I got from my own codebase is a poller service that uses RXJS to fire periodical requests against our backend. We have several usecases for such services and few among them are spawned right after the application starts. Since we are using RXJS, we also use ngOnDestroy() to cleanup any subscriptions.
So our current solution is to inject those services in our root AppModule and call myPoller.init() in either its constructor or ngDoBootstrap(). If we had ngOnInit() available in services it would be easier to keep everything in the service class where it belongs.

Normally such "violation" in the constructor is something I'm willing to accept, but things may get more clumsy if initialization needs some more business logic. For example - what if my poller service should target different backends depending on some external settings? Adding this logic to a constructor is a no-no then.

@lukaszbachman I would like you to define what "later" means in concrete terms. The issue I have is that for injector there is on "later". The only possible "later" for injector is to do it right after constructor and that does not seem useful.

I have a feeling that you want something "later" which makes sense in your application, but that "later" is app specific. From the injector point of view there is no concept of "later".

My guess is that they want this scenario: You provide a service in a component, when doing so the ngOnInit is triggered after the component's ngOnInit is triggered (if you implement the interface). This is the case for OnDestroy.

My guess is that they want this scenario: You provide a service in a component, when doing so the ngOnInit is triggered after the component's ngOnInit is triggered (if you implement the interface). This is the case for OnDestroy.

There is not a 1:1 relationship between the component and service, hence the above breaks down if there are two components asking for same instance of service at different times.

I would suggest that this feature request can be closed as it does not fit into the current mental model of how Injectors work.

@mhevery I really don't remember what I was doing at the time.

I think it might be related to eager loading services, but i'm not sure.

Anyway, there was no issue with the DI loop as I can use ngOnInit to init token post constructor.

But really, I don't remember clearly what was the case 2.5 years ago, corona and 2 little brats that destroyed a lot of my memory cells.

Anyway, there is some demand from people here... so we you might want to wait and hear more opinions.

Angular gives us 2 hooks for this:

Local and lazy-loaded services

For local (component-level) services and lazy-loaded services, we can use ApplicationInitStatus#donePromise as a signal like the following example of a local service:

// my-local.service.ts
import { ApplicationInitStatus, Injectable } from '@angular/core';

@Injectable()
export class MyLocalService {
  constructor(appInit: ApplicationInitStatus) {
	appInit.donePromise.then(() => this.onInit());
  }
  
  onInit(): void {
    // Set up or trigger side-effects
  }
}

This makes it possible to control side effect triggers in tests by replacing the ApplicationInitStatus service. Something like this:

// my-local.service.spec.ts
import { ApplicationInitStatus, Injectable } from '@angular/core';
import { TestBed } from '@angular/core/testing';

import { MyLocalService } from './my-local.service';

@Injectable()
class FakeApplicationInitStatus implements ApplicationInitStatus {
  done = false;
  donePromise = new Promise<void>((resolve, reject) => {
  	this.onAppInit = () => {
  	  this.done = true;
  	  resolve();
  	};
  	this.onAppInitFailure = () => {
      this.done = false;
  	  reject();
  	};
  });
  
  onAppInit: () => void;
  onAppInitFailure: () => void; 
}

describe(MyComponentService.name, () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        MyLocalService,
        { provide: ApplicationInitStatus, useClass: FakeApplicationInitStatus },
      ],
    });

    appInit = TestBed.inject(ApplicationInitStatus) as unknown as FakeApplicationInitStatus;
    service = TestBed.inject(MyLocalService);
  });
  
  let appInit: FakeApplicationInitStatus;
  let service: MyLocalService;
  
  it('side effects are triggered when the app has been initialized', () => {
    // Arrange
    const onInitSpy = spyOn(service, 'onInit');
    
    // Act
    appInit.onAppInit();
    
    // Assert
    expect(onInitSpy).toHaveBeenCalled();
  });
});

Global services

For global services, we can use an application initializer. Note that this will eagerly instantiate the service and doesn't work for lazy-loaded services.

If we want lazy initialization, that is only instantiating MyGlobalService when it's injected in a component or service, we can also use the ApplicationInitStatus technique as demonstrated for component-level services. The same goes for lazy-loaded services.

// my-global.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class MyGlobalService {
  today?: Date;

  onInit(): void {
    this.today = new Date();
  }
}
// my-global.initializer.ts
import { APP_INITIALIZER, FactoryProvider } from '@angular/core';

import { MyGlobalService } from './my-global.service';

function initializeMyGlobalFactory(myGlobal: MyGlobalService): () => void {
  return () => {
    myGlobal.onInit();
  };
}

export const myGlobalInitializer: FactoryProvider = {
  deps: [MyGlobalService],
  multi: true,
  provide: APP_INITIALIZER,
  useFactory: initializeMyGlobalFactory,
};

In the first test case of the following example test suite, the spy for MyGlobalService#onInit is probably not added before the initializer is run. Instead, we should assert that actual side effects have been run or we should replace collaborators (service dependencies) by configuring the Angular testing module before calling TestBed.inject.

// my-global.service.spec.ts
import { TestBed } from '@angular/core/testing';

import { myGlobalInitializer } from './my-global.initializer';
import { MyGlobalService } from './my-global.service';

describe(MyGlobalService.name, () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [myGlobalInitializer],
    });
    service = TestBed.inject(MyGlobalService);
  });
  
  let service: MyGlobalService;
  
  it('side effects are triggered when the app has been initialized (spy)', () => {
    // Arrange
    // The following spy is probably added too late
    const onInitSpy = spyOn(service, 'onInit');
    
    // Act
    
    // Assert
    expect(onInitSpy).toHaveBeenCalled();
  });
  
  it('side effects are triggered when the app has been initialized (inspect observable state)', () => {
    // Arrange
    
    // Act
    
    // Assert
    expect(service.today).toBeDefined();
  });
});

There might also be issues with tests involving asynchronous app initializers. In that case, try Spectacular's Application testing API.

@lukaszbachman I would like you to define what "later" means in concrete terms. The issue I have is that for injector there is on "later". The only possible "later" for injector is to do it right after constructor and that does not seem useful.

@mhevery To me the "later" means: after all necessary injectors have been initialized and the application is ready to work. So in these terms it's similar to the ApplicationInitStatus token.

Some abstract example could be a JokeGeneratorService. Say generating a good joke takes 5 minutes (jokesService.generate()). The application could start preparing its jokes as soon as it finishes bootstrapping and user starts his regular work, despite the fact that JokeComponent is not yet on the screen. Once the user starts his coffebreak he gets to the Jokes tab and can have an instant laugh instead of wasting his precious free time ;-)
My reasoning is that invoking the generate() method from the service constructor is an anti-pattern and invoking it from other parts of the application adds an unnecessary code coupling.
Right now I would likely handle that via ApplicationInitStatus, which is fine, but it certainly would make sense to me to have the ngOnInit() lifecycle hook being supported by the framework directly. Especially that services already respond to ngOnDestroy() and the presence of ApplicationInitStatus.

@lukaszbachman

That's a good use case for a bootstrap listener (APP_BOOTSTRAP_LISTENER). It's activated according to your definition of later.

Alternatively, it's possible to start the work in an application initializer (APP_INITIALIZER) but return true immediately, resolve the returned promise or emit the returned observable so that it doesn't block bootstrapping. But continuing to do the background fetching.

Considering that we know that jokes are always strictly necessary for the functionality of the app. Otherwise it wouldn't make sense and we should use route resolvers or side effects triggered by a component.

invoking the generate() method from the service constructor is an anti-pattern

Actually, I think that this only holds true for certain languages such as C++, where calling methods in general from a constructor can be dangerous, especially if the method is overridden in a subclass.

Moreover in Angular services you have the additional redirection of dependency injection. It is fair to say that calling long running functions or functions that access external resources is not a good idea from a constructor - Why? Because you have no control to turn off these calls in tests, etc. But in Angular you should be calling through to an injected service that would actually do this long running logic or access an external resource, which can be mocked out by configuring the injector with mock providers.

Therefore I think for Angular services there is actually nothing wrong with initializing everything in the constructor.

Aside: the main reason for ngOnInit in components is that they have a two step initialization. First the component instance itself is instantiated (calling the constructor). Then Angular connects this instance to the DOM and wires up all the inputs, which is guaranteed to happen before the call to ngOnInit(). Similarly there are hooks that ensure that child/view queries have been initialized in components.

These life-cycle hooks are necessary only for components. In services there is no additional step to worry about.

Thank you for your feedback. Those are good, valid points, @petebacondarwin.

But say we have a local data store such as an RxAngular State service or an NgRx Component Store service. In a component test we might want to use its data projection features but disable certain side effects in the context of a test so that we can easier control its internal state without having to replace the data store's collaborators.

For this use case we need a way to separate side effect setup from regular property initialization. I suggest something like what I listed in #23235 (comment)

In addition to your points, my point is that even for this and related use cases, Angular comes with 3 different hooks that we can use:

  • Application initializers (APP_INITIALIZER). Before bootstrap. They block bootstrapping.
  • Application initialized status (ApplicationInitStatus#donePromise). After application initializers, possibly before bootstrap depending on where and when we use it. Using this hook does not block bootstrapping.
  • Bootstrap listeners (APP_BOOTSTRAP_LISTENER). After bootstrap. They do not block any hooks.

I think that @LayZeeDK and I are in agreement, yes? There are already hooks in Angular to achieve the desired results, which I believe aligns with what @mhevery was saying about there being no obvious "later" for a service.

Maybe a good solution is to add documentation about this. Documentation for the mentioned application hooks could use some TLC. Guides or other sections could be added to angular.io, discussing this topic.

Actually, I think that this only holds true for certain languages such as C++, where calling methods in general from a constructor can be dangerous, especially if the method is overridden in a subclass.

Not being dangerous (which means, I suppose, "not crashing the program") does not mean that such issues cannot happen in JS.
Java has a concept of @PostConstruct when using beans. It allows calling methods and accessing properties that are initialized lazily (primarily due to lazy dependency injection). It also helps solving problems like calling a method that need a fully constructed this.
Most of the definitions of "later" that were discussed here may be solved by the existing solutions (application initializers, bootstrap listeners...).

However, it does not solve the problem of accessing properties in constructors, especially if using inheritance, when controlling the instructions' order is not an option:

// This service is created when a component needs it. So it can be initialized later in the app lifecycle.
@Injectable({providedIn: 'root'})
export class LazyService extends AbstractService {

    private readonly _ready: boolean;

    constructor(webSocket: WebSocket, otherService: OtherService) {
        super(webSocket);
        
        // Whatever the value is.
        this._ready = otherService.connected;
    }

    protected webSocketConnected(): void {
        console.log(this._ready); // undefined :(
    }
}
export abstract class AbstractService {
    protected readonly websocketReady$ = new Subject<boolean>();

    constructor(webSocket: WebSocket) {
        // Suppose connected$ is a BehaviorSubject that immediately emits, the subscribe callback will be called
        // in the constructor.
        // This causes the webSocketConnected() method to be called with a partially constructed "this".
        webSocket.connected$.subscribe(() => this.webSocketConnected());
    }
    
    protected abstract webSocketConnected(): void;
}

IMHO, "later" must mean "after the constructor has been called":

const service = new LazyService();
service.onInit();

Thanks for this use case @EclipseOnFire - it is definitely an interesting one. My immediate reaction is that an abstract class should be providing its own "hook" for when it wants to start its work (in this case subscribing). For example:

@Injectable({providedIn: 'root'})
export class LazyService extends AbstractService {
    private readonly _ready: boolean;

    constructor(webSocket: WebSocket, otherService: OtherService) {
        super(webSocket);        
        this._ready = otherService.connected;
        this.startListening();
    }

    protected webSocketConnected(): void {
        console.log(this._ready);
    }
}

export abstract class AbstractService {
    protected readonly websocketReady$ = new Subject<boolean>();

    constructor(private webSocket: WebSocket) {}

    startListening() {
        this.webSocket.connected$.subscribe(() => this.webSocketConnected());
    }
    
    protected abstract webSocketConnected(): void;
}

Of course it would be a solution, but I prefer to enforce this behavior at compile-time rather than expecting the user of the class to implement his class properly (and test it properly...).

Also, this is a recursive problem, since it also applies to non-abstract services that can be instanciated on their own.

Maybe some update? :)

I think the best scenario is to omit providing global services using APP_INITIALIZER
so instaed

export const myGlobalInitializer: FactoryProvider = {
  deps: [MyGlobalService],
  multi: true,
  provide: APP_INITIALIZER,
  useFactory: initializeMyGlobalFactory,
};

just in Injectable decorator

@Injectable({
   providedIn: 'root',
   immediateCreate: true // default false
})   

and this should create a new instance at the moment of creating new Injector (so constructor will be invoked).
If we want use it as a global service just use it with providedIn: 'root', if for specified scope then providers scope will do it for us ;)

I have a use case, that I did not see mentioned, I have a problem with inheritance where I have a utility class let's call it Base, and another class A which extends Base, Base has an init method that needs to loop through the class props of A, if I call this init method in the constructor of Base class it wont have access to A props because they have not been initialized (A props only gets initialized after the parent super is called), there are some workarounds to this problem but none of them are as simple as having ngOnInit that is call after the A is created

Since Angular version 14, we have the multi-provider ENVIRONMENT_INITIALIZER dependency injection token that supports initialization in lazy-loaded chunks as well as in eagerly loaded chunks:

// my-local.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class MyLocalService {  
  onInit(): void {
    // Set up or trigger side-effects
  }
}
// my-local.initializer.ts
import { ENVIRONMENT_INITIALIZER, FactoryProvider } from '@angular/core';

import { MyLocalService } from './my-local.service';

function initializeMyLocalFactory(myLocal: MyLocalService): () => void {
  return () => {
    myLocal.onInit();
  };
}

export const myLocalInitializer: FactoryProvider = {
  deps: [MyLocalService],
  multi: true,
  provide: ENVIRONMENT_INITIALIZER,
  useFactory: initializeMyLocalFactory,
};

myLocalInitializer can be passed to the providers option of bootstrapApplication, the providers option of an Angular module, or the providers option of a route, including lazy-loaded routes.

Hey @LayZeeDK , thanks for the answer, Im aware of that way great addition in angular 14, but imagine Base is part of a lib this config will need to be in the docs (or a factory that does that), and even if a user reads it some times he will forget to add it , I could also ask them every time they extend to override the parent constructor and call super.init like

class A extends Base(){
constructor(){
super();
super().init();
}
}

which is backward compatible and is a little code to add as well, I think a custom annotation could also do the trick, but again these are all extra steps the user needs to do after extending which they need to know and not forget to do it, but if the services support the ngOnInit cycle then nothing else needs to be done besides extend Base. So there are ways but none as easy to use and clean as supporting ngOnIinit on services.
By the way by supporting I mean ngOnInit is called once after creating an instance of the service so if two components use the service ngOnInit is called only once

One way to achieve this is by implementing a serviceOnInit factory function that's provided to useFactory:

interface OnInit {
  onInit(): void;
}

function serviceOnInit(service) {
  return function (...deps: any[]) {
    const instance: OnInit = new service(...deps);
    instance.onInit();
    return instance;
  }
}

@Injectable({
  providedIn: 'root'
})
class ApplesService {
  fruit = 'Apples';
}

@Injectable({
  providedIn: 'root'
})
class OrangesService {
  fruit = 'Oranges';
}

@Injectable({
  providedIn: 'root',
  useFactory: serviceOnInit(FruitsService),
  deps: [ApplesService, OrangesService]
})
class FruitsService implements OnInit {
  constructor(
    private applesService: ApplesService,
    private orangesService: OrangesService
  ) {}

  onInit(): void {
    console.log(this.applesService.fruit);
    console.log(this.orangesService.fruit);
  }
}

@gabrielguerrero
Wrap it in a provider factory

// libs/my/lib/src/lib/my-local.service.ts
import { Injectable } from '@angular/core';

@Injectable()
export class MyLocalService {  
  onInit(): void {
    // Set up or trigger side-effects
  }
}
// libs/my/lib/src/lib/my-local.initializer.ts
import { ENVIRONMENT_INITIALIZER, FactoryProvider } from '@angular/core';
import { MyLocalService } from './my-local.service';

function initializeMyLocalFactory(myLocal: MyLocalService): () => void {
  return () => {
    myLocal.onInit();
  };
}

export const myLocalInitializer: FactoryProvider = {
  deps: [MyLocalService],
  multi: true,
  provide: ENVIRONMENT_INITIALIZER,
  useFactory: initializeMyLocalFactory,
};
// libs/my/lib/src/lib/provide-my-local-service.ts
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
import { MyLocalService } from './my-local.service';

export function provideMyLocalService(): EnvironmentProviders {
  return makeEnvironmentProviders([MyLocalService, myLocalInitializer]);
}
// libs/my/lib/src/index.ts
export * from './lib/my-local.service';
export * from './lib/provide-my-local-service';
// apps/my-app/src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideMyLocalService } from '@my/lib';
import { AppComponent } from './app/app.component';

await bootstrapApplication(AppComponent, {
  providers: [provideMyLocalService()],
});

I use functions in the services constructor.
Same behavior as OnInit.

Hey @LayZeeDK @swseverance thanks. I ended up doing something similar to what you guys suggested and documented how to configure it, I still think supporting onInit in services will be a cleaner solution because reduces the factory config but thanks anyway for your answers

NgRx ComponentStore has examples of what this could look like, namely its OnStoreInit and OnStateInit lifecycle hooks.

@LayZeeDK is true,I forgot about that, I'll go check how they implemented it thanks :)

I do not get the purpose of this feature request. OnInit in components is required because some input fields might be initialized after constructor called, with the services you can use constructor as a initialization point

Hi, onInit could be useful with abstraction. Suppose we have class A extends B and class C extends B. B having abstract method M and we know we have to run that method at the constructor of A and C. Now we must call it in both constructors A and C. You cannot call method M in constructor B cause implementation of that method in A(or C) might use fields not initialized yet in B and that might throw exception. With OnInit placed in B class you could execute method M 'once' - assuming that B's onInit would be executed after all constructors(B,A or B,C).

@artkar22 Dont use class inheritance -> problem solved.

I really do not get why people try to build their vision of app architecture and want framework support for it...

@Lonli-Lokli so maybe we should remove class inheritance at all. I really do not get why people don't want to make frameworks better and debate about new features. Instead they post such a useless comments like yours

@artkar22 your case is just a small one use case among others, not everyone uses classes, and I would rather ask for #15280 support.

I understand that you will change your code after this feature be implemented but I will not because it's just small and not widely used approach with services.

@Lonli-Lokli I just pointed out that it can be useful. Feature is a feature. It is not mandatory and there is a lot of angular features that you don't have to use to make a good app. I had a case, it would nice to have OnInit so i shared it. I figured it out other way but it would be helpful. It is up to framework devs to collect feedback and decide.

To me it was useful when my service listened to several real-time events (WebSocket). I needed a "bootstrap" function that would unleash the events once the service is ready (= no more running the constructor).
What I ended up doing is writing a service that calls all services registered with a "REAL_TIME" injection token, but "OnInit" would be a bit fancier I think.

@Jethril I'm not sure what the connection to the component lifecycle OnInit is here? You're just talking about running code in services in the correct order, right?

@prewk To me, according to the issue description, OnInit has completely lost its connection to the component lifecycle.
What is needed is a means to get the service informed that all services that are initialized at the same level (component or app, as singleton) and its dependencies are properly initialized.

I see it like Spring Boot's ContextStartedEvent.

Over time, patterns and recommandations have evolved. We see some shift around components where it is now more than acceptable to run code in the constructor (injection context, signal inputs, signal queries etc).

Also there are no specific lifecycles around a service beside ngOnDestroy which is invoked when the injector of the service is destroyed. Bear in mind that a service can't be instantiated at all if its dependencies are not "available" and the service is instantiated the first time it's pull by the DI system.

Knowing all of this, I'll close the issue and recommand developers to use the constructor for initialization of a service

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.