RxJS - Conditionally add observable in a pipe - javascript

So i have a function like below
showLoader = () => <T>(source: Observable<T>) => {
LoaderService.loadPage.next(true);
return source.pipe(
finalize(() => {
LoaderService.loadPage.next(false);
})
);
};
And then i use it while making HTTP calls like below
return this.http.get(url).pipe(showLoader())
But let's say i encounter a scenario where i need the loader or any observable for that matter based on a condition; something like below
const loader : boolean = false
return this.http.get(url).pipe(concat(...), loader ? showLoader() : of(values))
I tried using the iif operator like below
const loader : boolean = false
return this.http.get(url).pipe(concat(...), mergeMap(v => iif(() => loader, showLoader(), of(v))))
and got the following error
TS2345: Argument of type '(source: Observable) => Observable'
is not assignable to parameter of type 'SubscribableOrPromise<{}>'.
Can someone hep me understand where i am going wrong and how to rectify the same

you could do it like this:
showLoader = (show: boolean = true) => <T>(source: Observable<T>) => {
if (!show) { // just go straight to source
return source;
}
return defer(() => { // defer makes sure you don't show the loader till actually subscribed
LoaderService.loadPage.next(true);
return source.pipe(
finalize(() => {
LoaderService.loadPage.next(false);
})
)
})
};
use:
return this.http.get(url).pipe(showLoader(false))
but the way you seem to be statically accessing LoaderService reeks of design issues and bugs in your future. FYI.

I would suggest something like following:
const startWithTap = (callback: () => void) =>
<T>(source: Observable<T>) => of({}).pipe(
startWith(callback),
switchMap(() => source)
);
const showLoader = () => <T>(source: Observable<T>) => concat(
iif(
() => loader,
source.pipe(
startWithTap(() => LoaderService.loadPage.next(true)),
finalize(() => {
LoaderService.loadPage.next(false);
})
),
EMPTY
), source);
return this.http.get(url).pipe(
showLoader()
);

Related

Property 'filter' does not exist on type 'unknown' Observable<Course[]> in Rxjs 6

As I'm trying to use filter like this.
beginnerCourses$: Observable<Course[]>;
advancedCourses$: Observable<Course[]>;
ngOnInit() {
const http$ = createHttpObservables('/api/courses');
const courses$ = http$.pipe(
map(res => Object.values(res["payload"]))
)
this.beginnerCourses$ = http$
.pipe(
map(courses => courses
.filter(course => course.category == 'BEGINNER')) //here problem is showing
);
this.advancedCourses$ = http$
.pipe(
map(courses => courses
.filter(course => course.category == 'ADVANCED')) //here problem is showing
);
courses$.subscribe(courses => {
this.beginnerCourses$ = courses.filter(course => course.category == 'BEGINNER'); //its working here
this.advancedCourses$ = courses.filter(course => course.category == 'ADVANCED'); //its working here
}, noop,
() => console.log('completed'));
}
The problem is Property 'filter' does not exist on type 'unknown' for
map(courses => courses.filter(course => course.category == 'BEGINNER'))) and map(courses => courses.filter(course => course.category == 'ADVANCED')))
As i'm enrolled in a course it shows this way and in his tutorial its working. But i don't know what i'm missing here.
Update 1:
Used it.
.pipe(
map((courses: any[]) => courses.filter(
course => course.category == 'BEGINNER')
)
);
But in Edge Console it shows.
ERROR TypeError: courses.filter is not a function
I was doing this tutorial and ran across the same problem.
I found that you need to specify the type for the courses$ observable:
const courses$: Observable<Course[]> = http$.pipe(
map(res => Object.values(res["payload"]))
)
But you also need to specify the courses$ observable and not the http$
observable when you assign it to beginnerCourses$ as below:
this.beginnerCourses$ = courses$
.pipe(
map(courses => courses
.filter(course => course.category == 'BEGINNER'))
);
This is a typescript error, it means that map doesn't know the type of observable it's transforming.
You can give typescript the typing informaiton a few different ways.
Try:
.pipe(
map((courses: any[]) => courses.filter(
course => course.category == 'BEGINNER')
)
);
Found the answer.
const http$: Observable<Course[]> = createHttpObservables('/api/courses');
referred it from the course github extra added is.
.pipe(
map(res => Object.values(res["payload"]))
)
with http$.
It required this Observable<Course[]> for using filter.
add Observable<Course[]> to courses$
const courses$: Observable<Course[]> = http$.pipe(
map(res => Object.values(res["payload"]))
)

Rxjs do something on first emit from multiple subscriptions

Is there a clean way to do something on first emit from multiple subscriptions ?
e.g.:
this.subscription1 = this.service.getData1().subscribe(data => {
this.data1 = data;
console.log('1');
});
this.subscription2 = this.service.getData2().subscribe(data => {
this.data2 = data2;
console.log('2');
});
// Do something after first emit from subscription1 AND subscription2
doSomething();
...
doSomething() {
console.log('Hello world !');
}
Output goal:
1
2
Hello world !
1
2
1
1
2
1
2
2
...
There've multiple times where I also needed such a isFirst operator that'll run some predicate only for the first emission. I've slapped together a quick custom operator that uses a single state variable first to decide if the emission is indeed first and run some predicate using the tap operator.
Since it uses tap internally it does not modify the source emission in any way. It only runs the passed predicate when the emission is indeed first.
Try the following
isFirst() operator
export const isFirst = (predicate: any) => {
let first = true;
return <T>(source: Observable<T>) => {
return source.pipe(
tap({
next: _ => {
if (first) {
predicate();
first = false;
}
}
})
);
};
};
For combining multiple streams that will be triggered when any of the source emits, you could use RxJS combineLatest function.
Example
import { Component } from "#angular/core";
import { timer, Observable, Subject, combineLatest } from "rxjs";
import { tap, takeUntil } from "rxjs/operators";
#Component({
selector: "my-app",
template: `<button (mouseup)="stop$.next()">Stop</button>`
})
export class AppComponent {
stop$ = new Subject<any>();
constructor() {
combineLatest(timer(2000, 1000), timer(3000, 500))
.pipe(
isFirst(_ => {
console.log("first");
}),
takeUntil(this.stop$)
)
.subscribe({
next: r => console.log("inside subscription:", r)
});
}
}
Working example: Stackblitz
In your case it might look something like
this.subscription = combineLatest(
this.service.getData1().pipe(
tap({
next: data => {
this.data1 = data;
console.log('1');
}
})
),
this.service.getData2().pipe(
tap({
next: data => {
this.data2 = data;
console.log('2');
}
})
)
).pipe(
isFirst(_ => {
console.log("first");
})
).subscribe({
next: r => console.log("inside subscription:", r)
});
The easiest strategy is to have a 3rd Observable that will perform this action.
See below example
const Observable1$ = timer(1000, 2000).pipe(
map(() => 1),
tap(console.log)
);
const Observable2$ = timer(1700, 1700).pipe(
map(() => 2),
tap(console.log)
);
const Observable3$ = combineLatest([Observable1$, Observable2$]).pipe(
take(1),
map(() => "Hello World!"),
tap(console.log)
);
Observable1$.subscribe();
Observable2$.subscribe();
Observable3$.subscribe();
The console output is as per below, since there are two subscribers to Observable1$ (i.e Observable1$ and Observable3$same as two subscribers toObservable2$(i.eObservable2$ and Observable3$ we see console logs 1 1 2 2 'hello world ...'
Here is the link to the stackblitz
In the above we notice that we get 2 subscriptions hence 2 console logs for each. To solve this we can use Subjects to generate new Observables and combine these instead
const track1Subject$ = new Subject();
const track1$ = track1Subject$.asObservable();
const track2Subject$ = new Subject();
const track2$ = track2Subject$.asObservable();
const Observable1$ = timer(1000, 2000).pipe(
map(() => 1),
tap(console.log),
tap(() => track1Subject$.next()),
take(5)
);
const Observable2$ = timer(1700, 1700).pipe(
map(() => 2),
tap(console.log),
tap(() => track2Subject$.next()),
take(5)
);
const Observable3$ = combineLatest([track1$, track2$]).pipe(
take(1),
map(() => "Hello World!"),
tap(console.log)
);
Observable1$.subscribe();
Observable2$.subscribe();
Observable3$.subscribe();
See Link to final solution
With some further restrictions, this problem becomes easier. Unfortunately, operators like combineLatest, and zip add extra structure to your data. I'll provide a solution with zip below, but it doesn't extend at all (if you want to add more logic downstream of your zip, you're out of luck in many cases).
General solution.
Assuming, however, that getData1 and getData2 are completely orthogonal (How they emit and how they are consumed by your app are not related in any predictable way), then a solution to this will require multiple subscriptions or a custom operator tasked with keeping track of emissions.
It's almost certainly the case that you can do something more elegant than this, but this is the most general solution I could think of that meets your very general criteria.
Here, I merge the service calls, tag each call, and pass through emissions until each call has emitted at least once.
merge(
this.service.getData1().pipe(
tap(_ => console.log('1')),
map(payload => ({fromData: 1, payload}))
),
this.service.getData2().pipe(
tap(_ => console.log('2')),
map(payload => ({fromData: 2, payload}))
)
).pipe(
// Custom Operator
s => defer(() => {
let fromData1 = false;
let fromData2 = false;
let done = false;
return s.pipe(
tap(({fromData}) => {
if(done) return;
if(fromData === 1) fromData1 = true;
if(fromData === 2) fromData2 = true;
if(fromData1 && fromData2){
done = true;
doSomething();
}
})
);
})
).subscribe(({fromData, payload}) => {
if(fromData === 1) this.data1 = payload;
if(fromData === 2) this.data2 = payload;
});
In the subscription, we have to separate out the two calls again. Since you're setting a global variable, you could throw that logic as a side effect in the tap operator for each call. This should have similar results.
merge(
this.service.getData1().pipe(
tap(datum => {
console.log('1')
this.data1 = datum;
),
map(payload => ({fromData: 1, payload}))
),
...
The zip Solution
This solution is much shorter to write but does come with some drawbacks.
zip(
this.service.getData1().pipe(
tap(datum => {
console.log('1')
this.data1 = datum;
)
),
this.service.getData2().pipe(
tap(datum => {
console.log('2')
this.data2 = datum;
)
)
).pipe(
map((payload, index) => {
if(index === 0) doSomething();
return payload;
})
).subscribe();
What is passed into your subscription is the service calls paired off. Here, you absolutely must set a global variable as a side effect of the original service call. The option of doing so in the subscription is lost (unless you want them set as pairs).

Emmit old value if SwitchMap has error in Rxjs

I am trying to get a member value from other web api in rxjs.
I did this in a pipe method with switchMap. But if there is a problem with getting the member value then I want to skip old model with values to next method.
So I dont want to return null after switchMap worked.
Here is my code:
(this.repository.getData(`employeecards/list/${this.currentUser.companyId}`) as Observable<EmployeeCard[]>)
.pipe(
flatMap(emp => emp),
tap(emp => emp),
switchMap((empCard: EmployeeCard) => this.repository.getData(`cards/${empCard.cardId}`),(empCard, card) => ({ empCard, card }) ),
//second subscribe
switchMap((emp: { empCard: EmployeeCard, card: LogisticCard }) => this.parraApiService.getBalanceByBarcode(emp.card.barcode),
(emp: { empCard: EmployeeCard, card: LogisticCard }, apiRes: ParraApiResult) => {
if (apiRes.response.isSuccess) {
//I use this subscribe only set to balance
emp.empCard.balance = apiRes.response.data['balance'];
}
return emp.empCard
}),
catchError((e) => {
return of([]); //I want to retun emp value
}),
reduce((acc, value) => acc.concat(value), [])
)
How can I solve this problem?
Thanks
I think you can achieve that by creating a closure:
(this.repository.getData(`employeecards/list/${this.currentUser.companyId}`) as Observable<EmployeeCard[]>)
.pipe(
flatMap(emp => emp),
tap(emp => emp),
switchMap((empCard: EmployeeCard) => this.repository.getData(`cards/${empCard.cardId}`), (empCard, card) => ({ empCard, card })),
switchMap(
(emp: { empCard: EmployeeCard, card: LogisticCard }) => this.parraApiService.getBalanceByBarcode(emp.card.barcode)
.pipe(
map((emp: { empCard: EmployeeCard, card: LogisticCard }, apiRes: ParraApiResult) => {
if (apiRes.response.isSuccess) {
emp.empCard.balance = apiRes.response.data['balance'];
}
return emp.empCard
}),
catchError((e) => {
// `emp` available because of closure
return of(emp);
}),
)
),
reduce((acc, value) => acc.concat(value), [])
)
Also notice that I gave up on switchMap's custom resultSelector, as it can easily be replaced with a map operator.

Multipe ajax calls in parallel and dispatching redux actions after success leads to "Uncaught Error: Actions must be plain objects"

I am new to rxjs and redux-observable. Trying to create an epic that allows me to do multiple parallel ajax calls and dispatches respective actions on success:
const loadPosters = (action$) => action$.pipe(
ofType(Types.LOAD_FILMS_SUCCESS),
switchMap(({ films }) =>
forkJoin(films.map(film =>
ajax
.getJSON(`https://api.themoviedb.org/3/search/movie?query=${film.title}`)
.pipe(
map(response => {
const [result] = response.results;
const poster = `http://image.tmdb.org/t/p/w500/${result.poster_path}`;
return Creators.savePoster(film, poster);
})
),
))
),
);
Creators.savePoster() is an action creator for an action named SAVE_POSTER. But, whenever i run my application, no such action is dispatched. Instead i get an error message in browser console:
Uncaught Error: Actions must be plain objects. Use custom middleware
for async actions.
edit
Tried a simplified version without forkJoin, sadly yielding the same result:
const loadPosters = (action$) => action$.pipe(
ofType(Types.INIT_SUCCESS),
mergeMap(({ films }) =>
films.map(film =>
ajax
.getJSON(`https://api.themoviedb.org/3/search/movie?query=${film.title}`)
.pipe(
map(response => {
const [result] = response.results;
const poster = `http://image.tmdb.org/t/p/w500/${result.poster_path}`;
console.log(Creators.savePoster(film, poster));
return Creators.savePoster(film, poster);
})
)
),
),
);
Appendix
Just for reference, I have another epic which does a simple ajax call which works fine:
const loadFilms = action$ => action$.pipe(
ofType(Types.INIT_REQUEST),
mergeMap(() =>
ajax
.getJSON('https://star-wars-api.herokuapp.com/films')
.pipe(
map(response => Creators.initSuccess(response))
),
),
);
The problem is that i don't return an Observable in my inner map. Changing:
return Creators.savePoster(film, poster);
to
return of(Creators.savePoster(film, poster));
makes it work.
By the way, if used with forkJoin it's also possible (and in my case better) to take the mapped results after all requests resolved and dispatch a single action instead of multiple ones:
const loadPosters = (action$) => action$.pipe(
ofType(Types.INIT_SUCCESS),
mergeMap(({ films }) =>
forkJoin(
films.map(film =>
ajax
.getJSON(`https://api.themoviedb.org/3/search/movie?query=${film.title}`)
.pipe(
map(response => ({ film, response }))
)
),
)
),
mergeMap(data => {
const posters = data.reduce((acc, { film, response}) => ({
...acc,
[film.id]: `http://image.tmdb.org/t/p/w500/${response.results[0].poster_path}`,
}), {});
return of(Creators.savePosters(posters));
})
);
In fact this is my favorite solution so far.

Setting page title dynamically in Angular

I have recently upgraded to Angular 6 and rxjs 6, since the upgrade, the following code to set the page title dynamically is no longer working
ngOnInit(): void {
this.router.events
.filter((event) => event instanceof NavigationEnd)
.map(() => this.activatedRoute)
.map((route) => {
while (route.firstChild) {
route = route.firstChild;
};
return route;
})
.filter((route) => route.outlet === 'primary')
.mergeMap((route) => route.data)
.subscribe((event) => this.titleService.setTitle(event['title']));
};
This gives me an error
this.router.events.filter is not a function
I tried wrapping the filter in a pipe like
this.router.events
.pipe(filter((event) => event instanceof NavigationEnd))
But I get the error
this.router.events.pipe(...).map is not a function
I have imported the filter like
import { filter, mergeMap } from 'rxjs/operators';
What am I missing here?
This is the correct way to use pipeable/lettables.
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
map(() => this.activatedRoute),
map((route) => {
while (route.firstChild) {
route = route.firstChild;
};
return route;
}),
filter((route) => route.outlet === 'primary'),
mergeMap((route) => route.data),
).subscribe((event) => this.titleService.setTitle(event['title']));
In RxJs 6 all the operators are pipeable, which means they should be used inside pipe method call. More info about that here.
So the code that you have should become something like:
this.router.events.pipe(
filter((event) => event instanceof NavigationEnd),
map(() => this.activatedRoute),
map((route) => {
while (route.firstChild) {
route = route.firstChild;
};
return route;
}),
filter((route) => route.outlet === 'primary'),
mergeMap((route) => route.data)
).subscribe((event) => this.titleService.setTitle(event['title']));
If you have a larger app I suggest you have a look at the rxjs-tslint project as it will allow you to update automatically the code.

Categories