Imagine I have 3 functions, taking some arguments and returning an observable. Logic would be (all would be a result of something async):
Get a cat.
Pet the cat.
Get food for the cat based on it's mood after petting it.
Sample code:
function getCat(): Observable<Cat> {
return Observable.of({ name: 'Larry' })
}
function petCat(cat: Cat): Observable<Mood> {
return Observable.of(Mood.HAPPY)
}
function getFood(cat: Cat, mood: Mood): Observable<Food> {
return Observable.of({ type: 'fish' })
}
I would like to end up with an Observable that's equivalent with this:
Observable.from([{ name: 'Larry' }, Mood.HAPPY, { type: 'fish' }])
I would like the functions to be invoked in sequence (obviously from the parameter signatures)
I would not like to delay the individual emits, in the final observable I'd like to get the results as soon as they arrive.
I could get it working with something like this:
function perform() {
return getCat().pipe(
mergeMap((cat) => concat(
of(cat),
petCat(cat).pipe(
mergeMap((mood) => concat(
of(mood),
getFood(cat, mood)
))
)
)
),
)
}
Question is, is there a better, more readable way of doing this (let's say I would have 5 of these that I would like to chain)?
You can try something like
getCat()
.pipe(
switchMap(cat => petCat(cat).pipe(map(mood => ({cat, mood})))),
switchMap(({cat, mood}) => getFood(cat, mood))
)
The whole idea is to use switchMap to switch from the source Observable to the Observable returned by the function passed to switchMap as parameter.
Maybe worth of mentioning is the use of map chained into the pipe of the first switchMap. That map operator makes sure that we keep cat as part of the parameters we pass the second, and last, switchMap.
UPDATE after comment
If the function perform has to emit all the 3 elements emitted by the 3 functions, then you may try something like the following
function perform() {
return getCat()
.pipe(
switchMap(cat => petCat(cat).pipe(map(mood => ({cat, mood})))),
switchMap(({cat, mood}) => getFood(cat, mood).pipe(map(food => ({cat, mood, food}))))
)
}
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
})
)
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);
}
EDIT: Added extra code in the filterEvents snippet for more context.
I'm not quite understanding what's going on with my code. I'm trying to pass an array into an action function inside of my Vuex store. If I return a Promise inside of that action function, then the parameter being passed isn't of type Array and is instead an Object, which results in the reject() error that I have for the Promise.
Here's some code for context:
filterEvents({ commit }, events) {
console.log(Array.isArray(events)); //this ends up false
console.log(events);
return new Promise((resolve, reject) => {
if (!Array.isArray(events)) {
reject("Invalid argument: is not of type Array.");
}
let filtered = events.filter((event) => {
let now = new Date();
let event_stop = new Date(event.stop_time);
if (event_stop >= now || event_stop == null) {
return event;
}
});
resolve(filtered);
});
}
Here's where I call filterEvents; inside of getEvents;
getEvents({ state, commit, dispatch }, searchParams) {
.....
eventful.getEvents(searchParams).then(async (res) => {
.....
console.log(Array.isArray(res.data.events.event)); //this ends up true
console.log(res.data.events.event);
/* where I call it */
await dispatch("filterEvents", res.data.events.event).then((res) => {
.....
});
}).catch((err) => {
.....
});
}
Here's the output from the Chrome developer console. First two outputs are from getEvents and last two are from filterEvents
Would really like an explanation as to why this is the case. I'm going to bet it's something small, but it's 3 a.m. at the moment and my brain can't wrap around why it's not of type Array when passed into filterEvents.
I always try to check the length prop of the array which helps me out in such cases.
...
return new Promise((resolve, reject) => {
if (!Array.isArray(events) && !events.length) {
reject("Invalid argument: is not of type Array.");
}
.....
});
...
I finally understood what my issue was after taking another look at the object that was being logged on the console. I did not know that Vuex actions HAD to have two arguments if you want to pass in a payload into that function. For example, I initially did this
filterEvents(events) {
.....
}
but what I really needed to do was
filterEvents(context, events) {
.....
}
The context argument is the object that allows you to do things such as commit and dispatch. I usually destructure the context object (i.e. { commit, dispatch} ), so I for some reason never thought twice about it. You don't have to destructure the context object to use commit and dispatch; if you don't it would just be like
context.commit('function', payload);
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 a provider that should allow me to return specific data from an API I need. I have this function that does it:
public getStoryCount(key: string, val: number) {
return this.client.getEntries({
'content_type': xxxxxxxxxxxxxx,
[key]: val,
}).then((entries:any) => {
return entries.total;
});
}
This is my first time really using promises, but I am trying to call this in a component to get the value. I want to be able to get the value entries.total which when I console.log gets output.
I am building out an array of data to use in my view like so:
this.homeScreen.push({
'count': Provider.getStoryCount('sys.id', xxxx)
});
When I console.log the Provider function I can see the value in the promise and it looks like this:
__zone_symbol__state : true
__zone_symbol__value : 13 // this is the value I need to get
How can I save that number 13 to my array homeScreen['count'] value?? Or what am I doing wrong??
You are currently returning the Promise and not the actual value. That means modifying your component code to this:
Provider.getStoryCount('sys.id', xxxx)
.then((entries:any) => {
this.homeScreen.push({
'count': entries.total
});
}
});
should work.
You could also make your Provider service get the value and store it as an Observable so components can then subscribe to the value.
Since promises are asynchronous, you are not actually returning entries.total like you might think.
You will probably need to provide your own callback function, or simply return the promise (generated by this.client.getEntries) and tack on a then to the result. It might look something like this:
public getStoryCount(key: string, val: number) {
return this.client.getEntries({
'content_type': xxxxxxxxxxxxxx,
[key]: val,
});
// The 'then' will be added later
}
// ...
// Get the promise from the provider, and invoke 'then' here.
var storyCountPromise = Provider.getStoryCount('sys.id', xxxx);
storyCountPromise.then((entries:any) => {
this.homeScreen.push({
'count': entries.total
});
});
It is an async operation. You need to pass a function in then:
Provider.getStoryCount('sys.id', xxxx)
.then((total) => {
this.homeScreen.push({
'count': total
});
});
Firstly, to map the result of the promise to another value, use map.
public getStoryCount(key: string, val: number) {
return this.client.getEntries({
'content_type': xxxxxxxxxxxxxx,
[key]: val,
}).map((entries:any) => {
return entries.total;
});
}
Then when calling the promise returning function use then to get the result
Provider.getStoryCount('sys.id', xxxx).then((total) => ...use total...);