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]) => { /* ... */ )
Related
So, i am new to RXJS, and i have checked a lot of stackoverflow and documentation before coming here and asking this, but i'm finding a hard time to make my logic work.
I have an Observable that will fetch a collection of documents and return them, and i use the pipe operator to make some changes, like using the map operator to change the object. So far, everything is fine.
The problem is here. Afterward, i need to run an "http request" for every document, in order to get specific data about them ("tags"). The http request is of course made as an Observable too, that needs to get subscribed on to fetch the data. However, the subscription takes some time, and the resulting object afterward doesn't have the required data.
let myFunction.pipe(
// mapping to add missing data needed for the front-end
map((results) => ({
...results,
documents: results._embedded.documents.map((document) => ({
...document,
tags: []
})),
})),
// mapping to loop through each document, and use the observable to get the tags with the document id
map((results) => {
let documents = results.documents.map((document: Document) => {
// get Tags for each document
let tagsToReturn = []
this.getDocumentTags(document.id)
.pipe(
// map function to return only the ids for each document, and not the complete tag object
map((tagsArray) => {
const modifiedTagsArray = tagsArray.map((tagObject: any) => {
if (tagObject !== undefined) {
return tagObject.id
}
})
return modifiedTagsArray
})
)
// the subscription to "actually" get the tags
.subscribe((tagsArray: number[]) => {
// Here the tags are found, but the latter code is executed first
// document.tags = tagsArray
tagsToReturn = tagsArray
})
// console.log(JSON.stringify(document))
// Here the tags are not found yet
console.log(JSON.stringify(tagsToReturn))
return { ...document, tags: tagsToReturn }
})
// I then, normally return the new documents with the tags for each document, but it is empty because the subscribe didn't return yet.
return {
_links: results._links,
page: results.page,
documents: documents,
}
}),
map((results) => {
results.documents.forEach((doc) => {
return this.addObservablesToDocument(doc)
})
return results
})
)
I have tried some solutions with switchmap, forkjoin, concat...etc but it didn't work, or i didn't find the correct way to use them. This is why i'm asking if there is a way to stop or another way to handle this problem.
I have tried using different operators like: mergemap, concat, switchmap to swich to the new request, but afterward, i can't have the global object.
I mostly tried to replicate/readapt this in some ways
By using mergemap combined with forkjoin, i was able to replicate what you were looking for.
Not really sure of how i can explain this, because i'm also not an expert coming to Rxjs, but i used the code from : this stackoverflow answer that i adapted
How i understand it is that, when using mergeMap in the pipe flow, you make sur that everything that get returned there, will be executed by the calling "subscribe()",then the mergeMap returns a forkJoin which is an observable for each document tags
I hope this can help
.pipe(
// mapping to add missing data needed for the front-end
map((results) => ({
...results,
documents: results._embedded.documents.map((document) => ({
...document,
tags: []
})),
})),
/******** Added Code *********/
mergeMap((result: ResultsNew<Document>) => {
let allTags = result._embedded.documents.map((document) =>
this.getDocumentTags(document.id).pipe(
map((tagsArray) => tagsArray.map((tagObject: any) => tagObject.id))
)
)
return forkJoin(...allTags).pipe(
map((idDataArray) => {
result._embedded.documents.forEach((eachDocument, index) => {
eachDocument.tags = idDataArray[index]
})
return {
page: result.page,
_links: result._links,
documents: result._embedded.documents,
}
})
)
}),
/******** Added Code *********/
map((results) => {
results.documents.forEach((doc) => {
return this.addObservablesToDocument(doc)
})
return results
})
)
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.
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);
}
I have a problem with conditional calls in RxJS.
The scenario is that I have multiple HTTP-Calls inside a forkjoin. But inside the calls there can be dependencies, like I get a boolean back from first call and if that is true the second call should be fired.
This is my code right now:
service.method(parameters).pipe(
tap((data: boolean) => {
foo.bar= data;
}),
concatMap(() =>
service
.method(parameters)
.pipe(
tap((data: KeyValue<number, string>[]) => {
if (true) {
foo.foo = data;
}
})
)
)
)
The problem that I have is that the method now always gets called. My goal is that the method only gets called when the parameter is true, to reduce the amount of calls. I hope somebody can help me.
You can try something like this
service.method(parameters).pipe(
tap((data: boolean) => {
foo.bar= data;
}),
concatMap((data) => data ? // you pass data as parameter and check if true
service // if data is true you return the Observable returned by the service method
.method(parameters)
.pipe(
tap((data: KeyValue<number, string>[]) => {
if (true) {
foo.foo = data;
}
})
) :
of(false) // if data is false you return an Observable containing what you decide it to contain, in this case 'false' but it could be everything
)
)
The idea is that you call the first time the service.method, pass the result of this call to concatMap and in there you decide, based on the parameter passed to concatMap whether to call again service.method and return the Observable it returns or return an Observable that you create within concatMap wrapping whatever value you want.
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>