I have a user library that is used across several Angular apps in my organization. As a refactor/new version, we are looking to move towards a state-based service, with an interface with something like this:
export interface UserService {
user: Observable<User>
getProfile(url: string): Observable<User>
updateProfile(url: string): Observable<User>
}
This way, consumers can bind components to UserService.user
knowing that the data will always be up-to-date, and call get/update functions in buttons or background activity, etc.
Clarification: The user library is meant to be used in apps that use ngRx
, as well as those that do not want something as heavy, but still want observable-based user binding.
So I wire up the functions like this (with slight variations) :
public getProfile(url: string): Observable<User> {
const obs = this._http.get<User>(url);
obs.subscribe(profile => this.setProfile(profile));
return obs;
}
Where setProfile
updates an internal Subscription that UserService.user
returns in a read-only manner.
Once of my consumer apps uses ngRx
for the rest of it's state management. When consuming my library, we notice some oddities:
When wrapped inside of an ngRx
effect, sometimes any functions using Angular's HttpClient
will be called several times. To rectify this issue, any function I have using HttpClient
ends with .publishLast().refCount()
to ensure the HTTP call is made only once.
If a call fails, the consumer cannot catch the exception. For instance, this.authService.getProfile().catch(error => alert(error))
will never get called. To get around this, I am now modifying my functions on their subscribe: obs.subscribe(profile => this.setProfile(profile), error => Observable.throw(error));
Is this normal behavior when implementing a "state-store" service in Angular?
Edit: Sample ngRx effect (please note that this is real implementation, what I posted above is way simplified from our actual implementation):
public BiographyUpdate: Observable<any> = this._actions.pipe(
ofType(AuthActionTypes.BIOGRAPHY_UPDATE),
map((action: BiographyUpdate) => action.payload),
switchMap(bio => this._auth.update(`${Environment.Endpoints.Users}/users/sync/biography`, action)
.map(profile => new BiographyUpdateSuccess(profile.biography))
.catch(error => {
console.log("AM I BEING HIT?");
return Observable.of(new BiographyUpdateFailure(error.toString()))
})
)
);
Once you call the subscribe
event, it is fired and the result resolved (correct result or error) inside the subscribe method. So at the time, you call this.authService.getProfile().catch
method, the asynchronous event is already fired and finished. That is why it's not calling.
You can either move the catch
operator in the getProfile
or you can return the observable from getProfile
without subscribing to it.
public getProfile(url: string): Observable<User> {
return this._http.get<User>(url)
.pipe( tap( profile => this.setProfile(profile)));
}
Now subscribe it through the authService
service
this.authService.getProfile()
.subscribe(profile => // do the work)
.catch(error => alert(error))