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.
Related
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]) => { /* ... */ )
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);
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}))))
)
}
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...);