Issue
I have a service that needs to asynchronously fetch some data to initialize. The constructor does something like
this.httpClient
.get<DataType>(`someUrl`)
.subscribe(data => this.data = data);
and a service like
someService() {
// do something with this.data
}
The issue is that the first time a view is displayed that use the service, the service is created on the fly by angular and the view calls it before it is fully initialized, causing errors. The next time the same view is displayed, the service is ready and it works.
I understand that my service could return a promise but this is more complex and I'de like to avoid it.
Is there a way to create the service lazily and wait for some asynchronous init before injecting it?
What I have tried so far...
I found a solution using an APP_INITIALIZER in app.module, then the service is created and initialized when the applications starts.
{ provide: APP_INITIALIZER, useFactory: onInitializeTheService, deps: [TheService], multi: true }
It works but the service is initialized for all the users, this is an issue because some don't have the autz to it.
I tried to use a factory for the service expecting it would allow to return a promise on the service but it does not. AppModule
{ provide: TheService, useFactory: createTheService },
export function createTheService() : Promise<TheService>
Solution
The App Initializer is for sure a possible strategy.
I would solve the issue with a ReplaySubject and a switchMap in your service. Something like this:
@Injectable({providedIn: 'root'})
export class TheService {
// the replay subject will store the value of InitData to be used
// by asynchronous operations.
private _someInitData$ = new ReplaySubject<SomeData>(1);
constructor() {
// TODO: add error / retry management
this.fetchInitData().subscribe(initData => {
this._someInitData$.next(initData);
this._someInitData$.complete();
});
}
private fetchInitData(): Observable<SomeData> {
...
}
// this is the method called by the components
doSomething(): Observable<Result> {
return this._someInitData$.pipe(
// switchMap allows to wait for the operation to complete,
// and then "switch" on the next fetch.
// It will be synchronous for every operation after the first.
switchMap(initData => {
return this.fetchSomethingUsingInitData(initData);
})
)
}
}
Very simple (there are more comment lines than actual code), and very appropriate if you don't need eager init.
Edit
To answer directly to the question:
Is there a way to create the service lazily and wait for some asynchronous init before injecting it?
No. There isn't. A factory function is expected to return a working service.
I expected the call to the service to perform some kind of async operation. From your comment, it seems that the method called by the components on the service can be synchronous, but must be called only after the service has been initialized.
I would return an observable in any case: it's by far the simplest option, and the change detection will cover your back for any sync problem.
If you really don't want to return an observable (which, again, you should), you have only a handful of options.
You could use an initialization action, but to solve your problem you would need to inject also all the services that are needed to read user data. Could be daunting.
You could have a property initialized: boolean = false;
on TheService
and set it to true only after the data has been initialized.
Then, in your component, you can use an *ngIf
(*ngIf="myService.initialized"
on a wrapper ng-container
) or whatever suits your situation to wait that the service has completed initialization.
AFAICT, this would be the only way to not use a form of Observable, because you would subside the synchronization to Angular's change detection mechanism in the template.
Anything that does not use change detection would still be susceptible to synchronization problems, so I would stick to an Observable, against every other option.
Answered By - Alberto Chiesa
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.