Convert a method with nested subscribes to an RxJS pipe - rxjs

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

Related

Calling/Subscribing to a function with parameters, that returns an observable

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

is there a better way to code for two dependent observables

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 )
);
}

How do I get my observable to have it's values for use in an NGRX effect

To be honest I am a total noob at NGRX and only limited experience in rxjs. But essentially I have code similar to this:
#Effect()
applyFilters = this.actions$.pipe(
ofType<ApplyFilters>(MarketplaceActions.ApplyFilters),
withLatestFrom(this.marketplaceStore.select(appliedFilters),
this.marketplaceStore.select(catalogCourses)),
withLatestFrom(([action, filters, courses]) => {
return [courses,
this.combineFilters([
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.TRAINING_TYPE),
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.INDUSTRIES)
])
];
}),
map(([courses, filters]) => {
console.log('[applyFilters effect] currently applied filters =>', filters);
console.log('courseFilters', filters);
const filteredCourses = (courses as ShareableCourse[]).filter(x => (filters as number[]).includes(+x.id));
console.log('all', courses);
console.log('filtered', filteredCourses);
return new SetCatalogCourses(filteredCourses);
})
);
Helper method:
private combineFilters(observables: Observable<number[]>[]): number[] {
if (!observables.some(x => x)) {
return [];
} else {
let collection$ = (observables[0]);
const result: number[] = [];
for (let i = 0; i < observables.length; i++) {
if (i >= 1) {
collection$ = concat(collection$, observables[i]) as Observable<number[]>;
}
}
collection$.subscribe((x: number[]) => x.forEach(y => result.push(y)));
return result;
}
}
So essentially the store objects gets populated, I can get them. I know that the observables of 'this.getCourseIdsFromFiltersByFilterType(args)' do work as on the console log of the 'filters' they are there. But the timing of the operation is wrong. I have been reading up and am just lost after trying SwitchMap, MergeMap, Fork. Everything seems to look okay but when I am trying to actually traverse the collections for the result of the observables from the service they are not realized yet. I am willing to try anything but in the simplest form the problem is this:
Two observables need to be called either in similar order or pretty close. Their 'results' are of type number[]. A complex class collection that has a property of 'id' that this number[] should be able to include. This works just fine when all the results are not async or in a component.(I event dummied static values with variables to check my 'filter' then 'includes' logic and it works) But in NGRX I am kind of lost as it needs a return method and I am simply not good enough at rxjs to formulate a way to make it happy and ensure the observables are fully realized for their values from services to be used appropriately. Again I can see that my console log of 'filters' is there. Yet when I do a 'length' of it, it's always zero so I know somewhere there is a timing problem. Any help is much appreciated.
If I understand the problem, you may want to try to substitute this
withLatestFrom(([action, filters, courses]) => {
return [courses,
this.combineFilters([
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.TRAINING_TYPE),
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.INDUSTRIES)
])
];
}),
with something like this
switchMap(([action, filters, courses]) => {
return forkJoin(
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.TRAINING_TYPE),
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.INDUSTRIES
).pipe(
map(([trainingFilters, industryFilters]) => {
return [courses, [...trainingFilters, ...industryFilters]]
})
}),
Now some explanations.
When you exit this
withLatestFrom(this.marketplaceStore.select(appliedFilters),
this.marketplaceStore.select(catalogCourses)),
you pass to the next operator this array [action, filters, courses].
The next operator has to call some remote APIs and therefore has to create a new Observable. So you are in a situation when an upstream Observable notifies something which is taken by an operator which create a new Observable. Similar situations are where operators such as switchMap, mergeMap (aka flatMap), concatMap and exhastMap have to be used. Such operators flatten the inner Observable and return its result. This is the reason why I would use one of these flattening operators. Why switchMap in your case? It is not really a short story. Maybe reading this can cast some light.
Now let's look at the function passed to switchMap
return forkJoin(
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.TRAINING_TYPE),
this.getCourseIdsFromFiltersByFilterType(filters, CatalogFilterType.INDUSTRIES
).pipe(
map(([trainingFilters, industryFilters]) => {
return [courses, [...trainingFilters, ...industryFilters]]
})
This function first executes 2 remote API calls in parallel via forkJoin, then take the result of these 2 calls and map it to a new Array containing both courses and the concatenation of trainingFilters and industryFilters

observable event filters and timeout

I am very new Rxjs observable and need help with two questions.
I have this piece of code:
const resultPromise = this.service.data
.filter(response => data.Id === 'dataResponse')
.filter((response: dataResponseMessage) => response.Values.Success)
.take(1)
.timeout(timeoutInSeconds)
.map((response: dataResponseMessage) => response.Values.Token)
.toPromise();
I have following basic questions:
1- How can I change .timeout(timeoutInSeconds) to add a message so that I can debug/log later which response it fails? I looked at .timeout syntax in rxjs and didn't see an option to include any message or something.
2-I know .filter((response: dataResponseMessage) => response.Values.Success) will filter to responses with response.Values.Success but is there a syntax where I can do like this for an observable:
const resultPromise = this.service.data
.filter(response => data.Id === 'dataResponse')
.magicSyntax((response: dataResponseMessage) => {
if (response.Values.Success) {
// do something
} else {
// do something else
}
});
Thank you so much in advance and sorry if these are basic/dumb questions.
First question
If you reach timeout the operator will return you an error which can be caught with .catch operator
const resultPromise = this.service.data
.filter(response => data.Id === 'dataResponse')
.filter((response: dataResponseMessage) => response.Values.Success)
.take(1)
.timeout(timeoutInSeconds)
.catch(e=>{
//do your timeout operation here ...
return Observable.Of(e)
})
.map((response: dataResponseMessage) => response.Values.Token)
.toPromise();
Second question simply replace magicSyntax with map or mergemap depends what you want to return from this operation. it is perfectly fine to do if in side the block.
I'm assuming you are using at least Rxjs version 5.5 which introduced pipeable operators. From the docs - these can "...be accessed in rxjs/operators (notice the pluralized "operators"). These are meant to be a better approach for pulling in just the operators you need than the "patch" operators found in rxjs/add/operator/*."
If you aren't using pipeable operators, instead of passing the operators to pipe() like I did below, you can chain them using the dot notation you use in your example.
I suggest referring to learnrxjs.io for some additional info about the operators in RxJS, paired with examples.
The RxJS team has also created a BETA documentation reference.
Explanation
I assumed the first filter is receiving the response and filtering by response.Id instead of data.Id. If that wasn't a typo, you can keep the filter the same.
I added an extra line between the operators for presentation only.
mergeMap is an operator that takes a function that returns an Observable, which it will automatically subscribe to. I'm returning of() here, which creates an Observable that just emits the value provided to it.
catch was renamed to catchError in RxJS 5.5, and pipeable operators were also added, which add support for the .pipe() operator.
If you don't want to do anything besides logging the error, you can return empty(), which will immediately call complete() on the source Observable without emitting anything. EMPTY is preferred if you are using version 6.
Optional: Instead of using filter() and then take(1), you could use the first() operator, which returns a boolean, just like filter(), and unsubscribes from the source Observable after it returns true once.
import {EMPTY, of} from 'rxjs';
import {catchError, filter, take, mergeMap, timeout} from 'rxjs/operators';
const resultPromise = service.data.pipe(
// I assumed you meant response.Id, instead of data.Id
filter((response: dataResponseMessage) => response.Id === 'dataResponse'),
take(1),
// mergeMap accepts a value emitted from the source Observable, and expects an Observable to be returned, which it subscribes to
mergeMap((response: dataResponseMessage) => {
if (response.Values.Success) {
return of('Success!!');
}
return of('Not Success');
}),
timeout(timeoutInMilliseconds),
// catch was renamed to catchError in version 5.5.0
catchError((error) => {
console.log(error);
return EMPTY; // The 'complete' handler will be called. This is a static property on Observable
// return empty(); might be what you need, depending on version.
})
).toPromise();
1- you can use .do() to console.log your response.
.filter(..).do(response => console.log(response))
2- you can use .mergeMap()

Angular 2/4 & RxJS - Subscriptions within forEach - Ensuring flow control doesn't continue until all Observables are complete

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)

Resources