RXJS convert single observable to array of observables - javascript

I have an API (getNewStories) that returns the data as an array of numbers(ids) such as [1,2,3,4...].
There is another API (getItem) that uses the number(id) and give its details.
How can I accomplish this with rxjs operators, so that I should only subscribe to it once and it gives me an array of the records with those ids?
I am able to accomplish this using 2 subscriptions, but I want it with one. Is it possible? and if it's, then, how?
this.hnService.getNewStories().subscribe(data => {
// data is [1,2,3,4,5]
// create an array of observables for all the ids and get the record for that id
const observables = data.map(item => this.hnService.getItem(item));
// use forkJoin to combine the array to single results variable
forkJoin(...observables).subscribe(results => {
this.stories = results;
});
});
with this I have to subscribe to both the APIs.

You were going the right direction with using forkJoin:
this.hnService.getNewStories()
.pipe(
concatMap(data => {
const items$ = data.map(item => this.hnService.getItem(item));
return forkJoin(...items$);
}),
)
.subscribe(allItems => ...);
forkJoin will wait until all source Observables complete and only then emit all results as a single array.

I think you can achieve this using a flattening operator like this.
this.hnService.getNewStories().pipe(
.mergeMap(data => this.hnService.getItem(data))
.subscribe(res => this.stories = res);
Other option can be to create two observable streams and use a combineLatest.

You can simply have implemented as in the the following snippet: (Yes, mergeAll flattens an observable containing an array, for further explanation refer to #Martin's post about the Best way to “flatten” an array inside an RxJS Observable
)
getNewStories().pipe(mergeAll(), concatMap(this.getItem), toArray()).subscribe()
You can try running the following snippet:
const { of } = rxjs;
const { concatMap, toArray, mergeAll } = rxjs.operators;
function getItem(x) {
return of({ item : x })
}
of([1, 2, 3, 4])
.pipe(
mergeAll(),
concatMap(getItem),
toArray()
)
.subscribe(console.log)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.6.2/rxjs.umd.min.js"></script>

Related

How to combine 2 or more observables so that you know which emitted?

let's say you have 2 observables:
const obs1$: Observable<string[]> = this.getObs1$();
const obs2$: Observable<string[]> = this.getObs2$();
i want to combine these 2 so that in subscription (or rxjs map) i know which emitted the values. I can't use combineLatest because for the other observable i just get the latest value it emitted at some point.
There doesn't seem to be a purely RxJS solution to this (but hopefully someone can prove me wrong on that!)
You could use a variable in the outter scope to track the emitting observable as a workaround
let trigger: TriggerEnum;
const obs1$: Observable<string[]> = this.getObs1$()
.pipe(tap(() => trigger = TriggerEnum.One));
const obs2$: Observable<string[]> = this.getObs2$()
.pipe(tap(() => trigger = TriggerEnum.Two));;
combineLatest(....).subscribe(
// check trigger value
);
From the docs as a just-in-case: be aware that combineLatest will not emit an initial value until each observable emits at least one value
You can use merge to combine the two observables into a single observable. Then, do what #martin suggested and map each source's emissions to a little structure that allows you identify the source:
const obs1$: Observable<number[]> = getNumbers();
const obs2$: Observable<string[]> = getLetters();
const combined$ = merge(
obs1$.pipe(map(data => ({ source: 'obs1$', data }))),
obs2$.pipe(map(data => ({ source: 'obs2$', data })))
);
combined$.subscribe(
({ source, data }) => console.log(`[${source}] data: ${data}`)
);
// OUTPUT:
//
// [obs1$] data: 1
// [obs2$] data: A
// [obs2$] data: A,B
// [obs2$] data: A,B,C
// [obs1$] data: 1,2
// [obs2$] data: A,B,C,D
...
Here's a little StackBlitz example.

Chaining subscription to multiple observables with parameter

I would like to subscribe two observables one after the other. The order is important and must be kept. The first observable returns a result itemId which must be passed to the second subscription. Currently, I use nested subscriptions, which is not very nice. What is the cleanest way to implement this?
// 1
this.widget$
.subscribe((widget) => {
const itemId: number = widget.data[0].itemId;
// 2
this.store
.select(DeviceHistoryStore.getItemHistoryEntries(this.deviceId, itemId))
.subscribe((deviceHistory) => {
const name = widget.name;
// Run code
});
});
Simply use a SwitchMap.
this.widget$.pipe(
switchMap(widget =>
this.store
.select(DeviceHistoryStore.getItemHistoryEntries(
this.deviceId,
widget.data[0].itemId
))
)
).subscribe(deviceHistory => { /* ... */ )
Edit:
If you want to access widget in the subscribe callback:
this.widget$.pipe(
switchMap(widget =>
combineLatest([
of(widget),
this.store
.select(DeviceHistoryStore.getItemHistoryEntries(
this.deviceId,
widget.data[0].itemId
))
])
)
).subscribe(([widget, deviceHistory]) => { /* ... */ )

Why is it returned when trying to execute the request Observable?

I am new to rxjs and am trying to do two requests. When I try to see the result, I get Observable.
copy() {
const obj = {};
this.create(skill)
.pipe(
mergeMap((res) => {
return [res, forkJoin(this.levels.map((level) => this.level(level)))];
}),
)
.subscribe((res) => {
console.log(res);
});
}
level(level) {
return this.create(level);
}
Output:
object of created skill,
Observable {_isScalar: false, _subscribe: ƒ}
I get the response of the first request normally and the second one comes to me as "Observable".
I'm not completely sure I understand what you're trying to do :-)
The function you pass to mergeMap() should "usually" return an observable. Currently, you are returning an array.
When you return array, mergeMap will simply emit each array element; which is why you receive those two emissions created skill, Observable.
However, if you return Observable, mergeMap will subscribe to it and emit.
I think this could work for you:
copy() {
this.create(skill).pipe(
mergeMap(createdSkill => forkJoin(this.levels.map(l => this.level(l))).pipe(
map(createdLevels => ([createdSkill, createdLevels]))
)
)
.subscribe(
([skill, levels]) => console.log({skill, levels})
);
}
It might be easier to follow if we break it down into smaller chunks:
createLevels() {
return forkJoin(this.levels.map(l => this.level(l));
}
copy() {
this.create(skill).pipe(
mergeMap(createdSkill => createLevels()).pipe(
map(createdLevels => ([createdSkill, createdLevels]))
)
)
.subscribe(
([skill, levels]) => console.log({skill, levels})
);
}
Looking it this way seems like we could instead build the copy() method in a simpler way:
copy(skill) {
forkJoin(
this.createSkill(skill),
this.createLevels()
)
.subscribe(
([skill, levels]) => console.log({skill, levels})
);
}
ForkJoin might not be the optimal operator here. I suggest having a look at the operator decision tree. I believe you want something like this though?
copy() {
const obj = {};
forkJoin(
[
this.create(skill),
...this.levels.map((level) => this.level(level))
]
).subscribe((res) => {
console.log(res);
});
}
level(level) {
return this.create(level);
}

Rxjs prevent subscribe in subscribe

I have an array of objects. I need to get array if ids, then call 2 APIs, then close the modal window.
My code is:
from(this.alerts)
.pipe(map(alert => alert._id))
.subscribe(alertIds => zip(
this.alertApi.someCall1(alertIds, ...),
this.alertApi.someCall2(alertIds, ...),
).subscribe(() => {
this.activeModal.close();
}),
);
Do you have any idea with preventing subscribe inside subscribe?
Use switchMap rxjs operator to avoid nested subscriptions.
from(this.alerts)
.pipe(
map(alert => alert._id),
switchMap(alertIds => zip(
this.alertApi.someCall1(alertIds, ...),
this.alertApi.someCall2(alertIds, ...)
))
)
.subscribe(() => {
this.activeModal.close();
);
More information on switchMap operator can be found here.
You can use forkJoin, which is similar to Promise.all, and switchMap.
See also: the RxJS docs, specifically example 6, which is similar to your situation.
from(this.alerts)
.pipe(
switchMap(alert =>
forkJoin(
this.alertApi.someCall1(alert._id),
this.alertApi.someCall2(alert._id)
)
)
)
).subscribe(() => {
this.activeModal.close();
});
Please note the resulting observable will only emit if all inner observables complete.

Multiple RxJS AJAX Requests

I'm using rxjs to make several http requests and I want to end up with an object that looks something like:
{
100: {
...response from api call...
},
205: {
...response from api call...
},
...etc...
}
Here's what I have so far:
const projectIds = [100, 205, 208, 300]
const source = Rx.Observable
.from(projectIds)
.flatMap(id => get(`projects/${id}/builds`))
.map(response => response['data'])
.zip(projectIds)
.toArray()
source.subscribe(pipelines => {
console.log(pipelines)
})
This gives me back an array of arrays where the first element is the response from the call and the second element is the id of the project.
The problem is that the response doesn't match the project id as the responses come back in different orders depending on which request completes first.
How can I preserve the order (or at least know which projectId goes with each response) while also ending up with an object at the end (currently is an array)?
Option 1
Instread of flatMap you could use concatMap, that should preserve the order.
Note: This won't make any concurrent requests though, if that is what you are looking for.
Option 2
If you want to make concurrent requests (at least from the RxJS side, depending on the browser this could still be limited) you could use some construct using forkJoin like the following:
const projectIds = [100, 205, 208, 300]
const source = Rx.Observable
.from(projectIds)
.map(id => get(`projects/${id}/builds`).pluck('data'))
.toArray()
.switchMap(requestArray => Rx.Observable.forkJoin(requestArray))
.zip(projectIds)
source.subscribe(pipelines => {
console.log(pipelines)
})
Just use the flatMap with elementSelector overload:
.flatMap(
projectId => getProjectDetails(projectId),
(projectId, details) => ({ id: projectId, details })
)
function getProjectDetails(id){
return get(`projects/${id}/builds`)
.map(response => response['data']);
}
This will let you combine the input argument and every output value from flatMap as you require, effectively preserving context. If you require the output order to stay the same you can use .concatMap but then all emissions are done after each other instead of concurrently.
Then finally use a .reduce to combine all objects back to one big emission:
.reduce((acc, curr) => acc[curr.id] = curr.details, {})

Categories