In the method below I want to call two observables. After the data from first observable (getUnrecoveredGearsExt- a http req) is returned I want to pass the data to the second observable (createUpdate- persist to indexDB). Is there a cleaner way to achieve this maybe using some of the rxjs operators. thanks
Note: after the successful completion of the second observable I want to return the data from the first Observable. The use case is get data from the backend and store locally in indexDB and if successful return data or error
public getAndUpdateUnrecoveredGears(cfr: string, maxResults?: number, excludeTripid?: string) : Observable<GearSet[]> {
return Observable.create((observer) => {
this.getUnrecoveredGearsExt(cfr,maxResults,excludeTripid).subscribe(
(gears: GearSet[]) => {
this.createUpdate(gears).subscribe(
() => {
observer.next(gears);
observer.complete();
},
(error) => {
observer.error(error);
}
);
},
(error) => {
observer.error(error);
}
);
});
}
Having nested .subscribe() methods is an anti-pattern of RxJS and can cause many issues. So it's a strong signal of when you need to use operators. Fortunately, there is one which simplifies your code.
public getAndUpdateUnrecoveredGears(cfr: string, maxResults?: number, excludeTripid?: string) : Observable<GearSet[]> {
return this.getUnrecoveredGearsExt(cfr,maxResults,excludeTripid).pipe(
concatMap((gears:GearSet[])=>this.createUpdate(gears))
);
}
Because we're dealing with HTTP requests, they'll emit one value then complete. For this, we can use concatMap(). This operator will wait until getUnrecoveredGearsExt() completes, and then will subscribe to createUpdate() using the value emitted from getUnrecoveredGearsExt(). The operator will then emit any values coming from this "inner observable".
Assuming createUpdate() is also an HTTP request, it will automatically send a complete signal after emitting the response.
The solution below works for me. The final issue was how to pass the previous result 'gears' out as a final result. This is achieved by using the combineLatest to pass the two results to the next map operator, which can then pass gears out.
public getAndUpdateUnrecoveredGearsAlt2(cfr: string, maxResults?: number, excludeTripid?: string): Observable<GearSet[]> {
return this.getUnrecoveredGearsExt(cfr,maxResults,excludeTripid).pipe(
switchMap((gears: GearSet[]) => { return combineLatest(this.createUpdate(gears),of(gears));}),
map(([temp, gears]) => gears )
);
}
Related
This is somewhat related to a previous question I asked. The feature$ function in that question returns an observable with a map that uses the parameter passed to the function:
feature$ = (feature: string): Observable<FeatureConfig | null> => {
return this.features$.pipe(
map((features: FeatureConfig[]) => {
return (
features.find((featureConfig: FeatureConfig) => {
return featureConfig.key === feature;
})?.value ?? null
);
})
);
};
This is then used like this elsewhere:
this.featureService
.feature$("featureName")
.subscribe((featureConfig: FeatureConfig) => {
...
});
Or:
someFeature$ = this.featureService.feature$("featureName");
The features$ observable is (I think, by definition) a hot observable as its value can change throughout the life of the observable and it never completes. While this seems to work for its intended purpose, I am just wondering what the effect this has when there are many subscribers to that feature$ function. I fear there might be some unintended behavior that I am not immediately noticing.
Is this a bad pattern in general? And if so, is there a better pattern to do something similar? That is, subscribe to an observable created with a parameter passed to a function.
For example, would something like this be preferred?
feature$ = (featureName: string): Observable<FeatureConfig | null> => {
return of(featureName).pipe(
mergeMap((feature: string) => combineLatest([of(feature), this.features$])),
map(([feature, features]: [string, FeatureConfig[]]) => {
return (
features.find((featureConfig: FeatureConfig) => {
return featureConfig.key === feature;
})?.value ?? null
);
})
);
};
Or does it matter?
The the second stream example is a bit overly complicated, your features$$ is a Behavior subject that might continuously updating itself. Your intend is only take in parameter and process through the features array and output the found feature, the first form of the code is more appropriate.
As the source stream is a BehaviorSubject you will always have a value once subscribe(), just don't forget to unsubcribe() to prevent memory leak. Alternatively use take(1) or first() operator before subscribe()
When you create an observable from a function you get a new instance of that stream, it is a hot observable but not shared(), so filtering on 'featureA' wouldn't affect result on filtering on 'featureB', and yes of() and combineLatest() really does nothing in your use case, as those are static and unchange function param
I have a code pattern that looks (simplified) like this:
loadDocs(): DocType {
this.http.get('/doc/1').subscribe(
(doc1) => {
this.http.get('/doc/' + '(something based on doc1)').subscribe(
(doc2) => {
return doc2;
}
);
}
);
}
What I want to do is convert that to a function that returns an Observable like:
loadDocs(): Observable<TypeDoc> {
return this.http.get('/doc/1').pipe(
// ???
);
}
I am sure this is very basic but so far I haven't been able to find an example of which RxJS
operators to use in this case.
you're looking for switchMap in this case:
loadDocs(): Observable<TypeDoc> {
return this.http.get('/doc/1').pipe(
switchMap(doc1 => this.http.get('/doc/' + '(something based on doc1)'))
);
}
mergeMap and concatMap would work the same here since http calls are single emission observables, the differences between them are only apparent with observables that emit multiple times. IMO switchMap is semantically most correct with http calls as it implies there will only be one active inner subscription, but opinions on this will vary.
They're all higher order operators that take the current value and map it into a new inner observable that they subscribe to and return the value
I got to refactor some code for a tremendous angular project I'm newly involved in. Some parts of the code lake of RxJS operators and do things like simple nested subscribes or copy/paste error handlers instead of using pipes.
Is it safe to assume that any simple 1 depth nested subscribes can be replaced by a mergeMap?
Let's take a login method like this :
private login() {
this.userService.logIn(this.param1, this.param2).subscribe((loginResult: {}) => {
this.userService.getInfo(this.param3).subscribe((user: UserModel) => {
// [the login logic]
},
(e) => {
// [the error handling logic]
})
}, (e) => {
// [The exact same copy/pasted error handling logic]
});
}
Is it safe to replace it with this?
private login() {
this.userService.logIn(this.param1, this.param2)
.pipe(
mergeMap((x) => this.userService.getInfo(this.param3))
)
.subscribe((user: UserModel) => {
// [the login logic that redirects to "my account" page]
},
(e) => {
// [the error handling logic]
});
}
What would be the difference with flatMap or switchMap here, for instance?
For your case I will go with SwitchMap.
mergeMap/FlatMap: transforms each emitted item to a new observable as defined by a function. Executes the executions in parallel and merges the results (aka flatMap) order doesn't matter.
swicthMap: transforms each emitted item to a new observable as defined by a function.
-Subscribers from prior inner observable.
-Subscribers to new inner observable.
-Inner observable are merged to the output stream.
-When you submit a new observable, multiple times the previous one gets cancelled and only emits the last one which is awesome for performance.
I'm new to RxJS and trying to wrap my brain around how I should be writing my code. I'm trying to write a function that extends an existing http which returns an observable array of data. I'd like to then loop over the array and make an http request on each object and return the new array with the modified data.
Here's what I have so far:
private mapEligibilitiesToBulk(bulkWarranties: Observable<any[]>): Observable<IDevice[]> {
const warranties: IDevice[] = [];
bulkWarranties.subscribe((bulk: any[]) => {
for (let warranty of bulk) {
// Check if another device already has the information
const foundIndex = warranties.findIndex((extended: IDevice) => {
try {
return warranty.device.stockKeepingId.equals(extended.part.partNumber);
} catch (err) {
return false;
}
});
// Fetch the information if not
if (foundIndex > -1) {
warranty.eligibilityOptions = warranties[foundIndex];
} else {
this.getDevices(warranty.device.deviceId.serialNumber).subscribe((devices: IDevice[]) => {
warranty = devices[0];
}); // http request that returns an observable of IDevice
}
warranties.push(warranty);
}
});
return observableOf(warranties);
}
Currently, my code returns an observable array immediately, however, its empty and doesn't react the way I'd like. Any advice or recommended reading would be greatly appreciated!
Without knowing a lot more about your data and what would make sense, it is impossible to give you the exact code you would need. However, I made some assumptions and put together this StackBlitz to show one possible way to approach this. The big assumption here is that the data is groupable and what you are actually trying to achieve is making only a single http call for each unique warranty.device.stockKeepingId.
I offer this code as a starting point for you, in the hopes it gets you a little closer to what you are trying to achieve. From the StackBlitz, here is the relevant method:
public mapEligibilitiesToBulk(bulk: Warranty[]): Observable<IDevice[]> {
return from(bulk).pipe(
tap(warranty => console.log('in tap - warranty is ', warranty)),
groupBy(warranty => warranty.device.stockKeepingId),
mergeMap(group$ => group$.pipe(reduce((acc, cur) => [...acc, cur], []))),
tap(group => console.log('in tap - group is ', group)),
concatMap(group => this.getDevices(group[0].device.deviceId.serialNumber)),
tap(device => console.log('in tap - got this device back from api: ', device)),
toArray()
)
}
A couple of things to note:
Be sure to open up the console to see the results.
I changed the first parameter to an array rather than an observable, assuming you need a complete array to start with. Let me know if you want this to extend an existing observable, that is quite simple to achieve.
I put in some tap()s so you can see what the code does at two of the important points.
In the StackBlitz currently the getDevices() returns the same thing for every call, I did this for simplicity in mocking, not because I believe it would function that way. :)
The goal is to iterate through a collection of IDs, making an HTTP call for each ID. For each ID, I'm using a service with a get() method that returns an Observable. Each time the get() method is called, I'm subscribing to the returning Observable and trying to push the result into an array, which will eventually get passed on to a different method for a new operation.
Relevant service method:
public get(departmentId: number): Observable<IDepartmentModel> {
return super.get<IDepartmentModel>(departmentId);
}
note: the super class is leveraging Angular Http, which is well tested and confirmed to be working correctly. The problem with the logic isn't here...
Relevant component methods:
note the departmentService.get() call that's being called several times within the forEach.
setInitialDepartmentsAssignedGridData(): void {
this.settingsForDropdownSelectedCompanyId = this.userForm.get('defaultCompany').get('defaultCompanyId').value;
let departments: IDepartmentModel[] = [];
this.userService.user.getValue() //confirmed: valid user is being pulled back from the userService (logic is fine here..)
.userCompanies.find(comp => comp.companyId === this.settingsForDropdownSelectedCompanyId) // getting a valid match here (logic is fine here..)
.departmentIds.forEach(deptId => this.departmentService.get(deptId).first().subscribe(dept => { // getting a valid department back here (logic is fine here...)
departments.push(dept); // HERE LIES THE PROBLEM
}));
this.setDepartmentsAssignedRowData(departments);
}
setDepartmentsAssignedRowData(departments: IDepartmentModel[]): void {
console.log('setDeptAssignedRowData called'); // confirmed: method is getting called...
console.log(departments); // confirmed: fully-composed collection of departments logged to the console...
departments.forEach(dept => {
console.log(dept);
}); // Y U NO WORK!?
departments.map((department) => {
console.log(department); // Y U NO WORK?
this.departmentAssignedRowData.push({
departmentId: department.id,
departmentName: department.name
});
});
this.departmentAssignedGridOptions.api.setRowData(this.departmentAssignedRowData);
}
The problem is, although what's getting logged to the console is a fully-composed department-objects array, it's not TRULY "there"; what's getting passed to setDepartmentsAssignedRowData is an empty array.
I'm sure what's happening is that the async operations are not complete before the departments array gets passed to the second method. Some of what I've read online says to use forkJoin, but I can't see how that will look in this context. I've also read concatMap may work, but again, in this context, I'm not sure how to make that work...
In this context, how do I leverage RxJS to make sure the intended, fully-composed departments array is truly ready to be passed?
thanks for any insight you can provide. help is much appreciated!
You are correct, you need forkJoin
let observableArray = this.userService.user.getValue()
.userCompanies.find(comp => comp.companyId === this.settingsForDropdownSelectedCompanyId)
.departmentIds.map(deptId => this.departmentService.get(deptId)) // map is building out an array of observables
This will be an array of http request observables that you want to make in parallel. Now you can pass this array to forkJoin.
Observable.forkJoin(...observableArray)
The return of forkJoin will be an array of results from observableArray. forkJoin will not emit to the next operator in the sequence until all of the observables in observableArray have completed (so when all of the http requests have finished)
So altogether the code will be
let observableArray = this.userService.user.getValue()
.userCompanies.find(comp => comp.companyId === this.settingsForDropdownSelectedCompanyId)
.departmentIds.map(deptId => this.departmentService.get(deptId));
Observable.forkJoin(...observableArray).subscribe(res => {
// res = [resId0, resId1, resId2, ..., resIdX];
});
You mentioned passing the result to another operator. If that operator is another http request where you pass an array of data (from forkJoin), then you can use the flatMap operator.
Observable.forkJoin(...observableArray)
.flatMap(res => {
return this.otherApi(res);
})
.subscribe(res => {
// res is the result of the otherApi call
});
flatMap will chain your api requests together. So altogether what is happening is
run array of observables in parallel
once complete, run second api (otherApi)