How to use Rx.Observable.prototype.let operator? - rxjs

The example and explanation of the let operator (https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/let.md) is not clear. Anyone has a good example/explanation how the let operator works, and when we should use it?

&tldr;
It is a convenience function for being able to compartmentalize logic and inject it into a pipeline.
Longer Explanation
The source is probably the most definitive explanation. It is really just passing a function which gets called with a source Observable.
Rx.Observable.prototype.let = function(fn) {
return fn(this);
}
The utility of this is that we can create or pre-define a pipeline that you want to reuse for multiple sources. Consider a common trope for Rx, the reactive search bar:
// Listen to a key up event on the search bar
// and emit the value of the search
Rx.Observable.fromEvent(searchBar, 'keyup', e => e.target.value)
// Don't search too eagerly
.filter(text => text.length > 3)
.debounceTime(500)
//Search logic
.flatMap(text => $.getJSON(`my/search/api?q=${text}`))
.flatMap({results} => results)
//Handler
.subscribe(appendToList);
The above should give a basic sense of the structure of how a pipeline might be created. If we wanted to try and abstract some of this logic either to clean up the code or to be able to use it elsewhere it can be a little tricky, because it usually means creating a new operator (and that has its own headaches).
The solution is a relatively simple approach of pulling common logic into a function that can be passed a source Observable and will return a new Observable with that logic applied.
So the above might become:
//Defined in pipelines.js
function filterBuilder(minText, debounceTime) {
return (source) =>
source.filter(text => text.length > minText)
.debounce(debounceTime);
}
function queryBuilder(baseUrl) {
return (source) =>
source.flatMap(text => $.getJSON(`${baseUrl}?q=${text}`))
.flatMap({results} => results);
}
//In your application code
Rx.Observable.fromEvent(searchBar, 'keyup', e => e.target.value)
.let(filterBuilder(3, 500))
.let(queryBuilder('my/search/api'))
.subscribe(appendResults);

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

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

How do I refactor a traditional synchronous loop with RxJS?

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. :)

RxJS BehaviorSubject with custom create logic

Because BehaviorSubject extends Subject and Subject extends Observable, all of those three have static .create(observer) method to create them using custom values emission logic.
I' able to use with good result Observable.create(observer), for instance:
a = Rx.Observable.create(obs => {
setInterval(() => {
obs.next('tick');
}, 500)
})
s = a.subscribe(v => console.log(v))
Gives me expected output (tick every 500ms)
But when I replace Observable with Subject/BehaviorSubject, it's not so willing to get up and running:
a = Rx.Subject.create(obs => {
setInterval(() => {
obs.next('tick');
}, 500)
})
s = a.subscribe(v => console.log(v)); // Nothing
a.next(5); // Still nothing
Basically, subject seems to work as intended to only if they are created via new operator like below:
a = new Rx.Subject();
s = a.subscribe(v => {console.log(v)});
a.next(5) // Ok, got value here
Even if I try to use non-parametrized create method, which invocation shall boil down to same result as using new:
a = Rx.Subject.create();
I'm still unable to force it to emit values.
I'm aware that subjects are designed to receive values from outside world (not to generate them internally as Observables), thus subject shall be triggered by external code with subject.next('value'), but I was just curios that if they are strictly related to Observables, logic behind create and further behavior shall be same...
Can anyone explain, why usage of create on Subject (even if they are not designed to work this way, but still it shall be possible) does not work as supposed to?

Converting callback hell to observable chain

I have been working with a convention where my functions return observables in order to achieve a forced sequential series of function calls that each pass a returned value to their following "callback" function. But After reading and watching tutorials, it seems as though I can do this better with what I think is flatmap. I think I am close with this advice https://stackoverflow.com/a/34701912/2621091 though I am not starting with a promise. Below I have listed and example that I am hoping for help in cleaning up with advice on a nicer approach. I am very grateful for help you could offer:
grandparentFunction().subscribe(grandparentreturnobj => {
... oprate upon grandparentreturnobj ...
});
grandparentFunction() {
let _self = this;
return Observable.create((observer) => {
...
_self.parentFunction().subscribe(parentreturnobj => {
...
_self.childFunction( parentreturnobj ).subscribe(childreturnobj => {
...
observer.next( grandparentreturnobj );
observer.complete();
});
});
});
}
parentFunction() {
let _self = this;
return Observable.create((observer) => {
...
observer.next( parentreturnobj );
observer.complete();
}
}
childFunction() {
let _self = this;
return Observable.create((observer) => {
...
observer.next( childreturnobj );
observer.complete();
}
}
The general rule-of-thumb in RxJS is that you should really try to avoid creating hand-made, custom Observables (i.e., using Observable.create()) unless you know what you're doing, and can't avoid it. There are some tricky semantics that can easily cause subtle problems if you don't have a firm grasp of the RxJS 'contract', so it's usually better to try to use an existing Observable creation function. Better yet, create Observables via applying operators on an existing Observable, and return that.
In terms of specific critiques of your example code, you're right that you should be using .flatMap() to create Observable function chains. The nested Observable.create()s you currently have are not very Rx-like, and suffer from the same problems 'callback hell'-style code has.
Here's an example of doing the same thing your example does, but in a more idiomatic Rx style. doStuff() is our asynchronous function that we want to create. doStuff() needs to call the asynchronous function step1(), chain its result into the asynchronous function step2(), then do some further operations on the result, and return the final result to doStuff()'s caller.
function doStuff(thingToMake) {
return step1(thingToMake)
.flatMap((step1Result) => step2(step1Result))
.map((step2Result) => {
let doStuffResult = `${step2Result}, and then we're done`;
// ...
return doStuffResult;
});
}
function step1(thingToMake) {
let result = `To make a ${thingToMake}, first we do step 1`;
// ...
return Rx.Observable.of(result);
}
function step2(prevSteps) {
let result = `${prevSteps}, then we do step 2`
// ...
return Rx.Observable.of(result);
}
doStuff('chain').subscribe(
(doStuffResult) => console.log(`Here's how you make a chain: ${doStuffResult}`),
(err) => console.error(`Oh no, doStuff failed!`, err),
() => console.debug(`doStuff is done making stuff`)
)
Rx.Observable.of(x) is an example of an existing Observable creator function. It just creates an Observable that returns x, then completes.

Resources