Reputation: 352
I have the following code where I need to get a value from two separate observables (they are actually promises but have been converted to observables using "from") and then use the values returned from each to make a final API call where both the values are used.
The method should return the result from the API call which is an object called "AppUser".
My code also has some logic in it to retry from a backup API if the primary API fails and then if the backup fails it should return the value from local storage.
This code does work but having read up about nested observables not being good practice I wondered if there is a better way to write this? Any advice would be appreciated.
// Get app user
getAppUser(getFromLocal: boolean = false, isRetry: boolean = false): Observable<AppUser> {
// If "get from local" is true
if (getFromLocal) {
return this.getAppUserFromLocal();
} else {
// Create observables to get values from local storage
const customerObs = from(this.authLocal.getCustomerId());
const authTokenObs = from(this.authLocal.getAuthToken());
return customerObs
.pipe(mergeMap((customerId) => {
return authTokenObs
.pipe(mergeMap((authToken) => {
// API Header
const headers = new HttpHeaders({
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${authToken}`
});
const url = (!isRetry ? this.baseUrl : this.backupBaseUrl) + '/GetAppUser?customerid=' + customerId;
return this.http.get(url, { headers })
.pipe(map((appUser: AppUser) => {
// Set AppUser in local storage
this.storage.set('appUser', appUser);
return appUser;
}),
catchError(e => {
if (!isRetry) {
// Try again using backup API
return this.getAppUser(getFromLocal, true);
} else {
// Try again from local storage
return this.getAppUser(true, true);
}
}));
}));
}));
}
}
UPDATED with potential solution
I have updated as per the answers with the following potential solution:
// Get app user
getAppUser(getFromLocal: boolean = false, isRetry: boolean = false): Observable<AppUser> {
// If "get from local" is true
if (getFromLocal) {
return this.getAppUserFromLocal();
} else {
// Create observables to get values from local storage
const customerId$ = from(this.authLocal.getCustomerId());
const authToken$ = from(this.authLocal.getAuthToken());
return forkJoin([
customerId$,
authToken$
]).pipe(
switchMap(([customerId, authToken]) => {
const headers = new HttpHeaders({
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${authToken}`
});
const url = (!isRetry ? this.baseUrl : this.backupBaseUrl) + '/GetAppUser?customerId=' + customerId;
return this.http.get(url, { headers })
.pipe(
map((appUser: AppUser) => {
appUser.AuthToken = authToken;
return appUser;
}),
catchError(e => {
if (!isRetry) {
// Try again using backup API
return this.getAppUser(getFromLocal, true);
} else {
// Try again from local storage
return this.getAppUser(true, true);
}
}));
}),
tap((appUser: AppUser) => {
// Set AppUser in local storage
this.storage.set('appUser', appUser);
}),
);
Upvotes: 0
Views: 253
Reputation: 2910
So you want two observables to get their values first, and then you want to jump over to your third observable (api observable). A general way of solving this problem could be with combineLatest and switchMap:
combineLatest([ customerObs, authTokenObs ])
.pipe(
switchMap(([customerId, authToken]) => {
// Do the code that creates your api observable
return apiObs;
})
)
.subscribe(resultFromApiCall => console.log(resultFromApiCall));
Upvotes: 1
Reputation: 3571
You can use forkJoin
to emit both values, especially because you know they will complete as they are from Promises (I've also renamed your Observables to better follow norms) :
return forkJoin({
customerId: customerId$,
authToken: authToken$
}).pipe(
switchMap(({ customerId: string, authToken: string }) => {
const headers = new HttpHeaders({
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${authToken}`
});
const url = (!isRetry ? this.baseUrl : this.backupBaseUrl) + '/GetAppUser?customerid=' + customerId;
return this.http.get(url, { headers }).pipe(retry(1));
}),
tap(user => this.storage.set('appUser', appUser)),
);
The only error handling here is the retry
operator (you can choose to retry more times), and I've used tap
for calling setUser
as it is the recommended method for side effects.
Upvotes: 1