RxJs Observables nested subscriptions with takeUntil - rxjs

How am I supposed to get rid of these nested subscriptions? I thought to do that using concatMap or mergeMap - if you agree, how to I handle the takeUntil for inner subscriptions being different than the outer one? I am quite new to operators and RxJs.
this.myService.selectedCustomerId
.pipe(
takeUntil(this.unsubscribe),
).subscribe((customerId: number) => {
// Do some stuff...
this.anotherService.hasPermission("ROLE1", customerId).pipe(
takeUntil(this.cancel),
).subscribe(hasPermission => this.hasPermissionForRole1 = hasPermission);
this.anotherService.hasPermission("ROLE2", customerId).pipe(
takeUntil(this.cancel),
).subscribe(hasPermission => this.hasPermissionForRole2 = hasPermission);
this.anotherService.hasPermission("ROLE3", customerId).pipe(
takeUntil(this.cancel),
).subscribe(hasPermission => this.hasPermissionForRole3 = hasPermission);
}
);

you can achieve it this way:
this.myService.selectedCustomerId
.pipe(
takeUntil(this.unsubscribe),
mergeMap(customerId: number) => {
const roles = ["ROLE1", "ROLE2", "ROLE3"];
return forkJoin(
roles.map(role => this.anotherService.hasPermission(role, customerId))
)
}
).subscribe(([role1, role2, role3]) => {
// Do some stuff...
}

Related

How do you return combineLatest results along with switchMap values?

This is basically what I am after. Just not sure the best way to return the combination of all three in the switchmap.
unsubscribes = combineLatest(
apiCall1,
apiCall12,
).pipe(
switchMap(([apiCall1Res, apiCall2Res]) => {
return apiCall3(apiCall1Res.Id)
})
).subscribe(([apiCall1Res, apiCall2Res, apiCall3Res]) => {
///Do work
})
If apiCall3 should after 1 and 2:
combineLatest(
apiCall1,
apiCall12,
).pipe(
switchMap(([apiCall1Res, apiCall2Res]) => {
return apiCall3(apiCall1Res.Id)
.pipe(map(apiCall3Res => [apiCall1Res, apiCall2Res, apiCall3Res]));
})
With ... you can save some space here:
combineLatest(
apiCall1,
apiCall12,
).pipe(
switchMap(results => apiCall3(apiCall1Res.Id)
.pipe(map(apicallResult3 => [...result, apicallResult3])
)
)
The sequence is correct, you just need to adjust how you treat the return values. When you use switchMap you transform the output of the observable sequence from the type you are receiving to the type of output of the observable you provide on the switchMap return. So you just must create an observable that returns the 3 values. You can do it by mapping the flow of the apiCall3 joining with the other two.
I propose one solution that can be adjusted to match your specific scenario if you need more. I created mock objects in order to make the sample directly executable for testing.
You can see the sample running with mock objects on the following stackblitz I created for you:
Editor: https://stackblitz.com/edit/rxjs-hjyrvn?devtoolsheight=33&file=index.ts
App: https://rxjs-hjyrvn.stackblitz.io
import { combineLatest, of, timer } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';
// Mock objects...
const apiCall1 = timer(1).pipe(map(() => ({id: 1})));
const apiCall2 = timer(2).pipe(map(() => 2));
// apiCall3 mock created bellow on the fly...
let r1, r2; // <-- to save partial results because they are cutted from the flow bellow...
const source =
combineLatest(
apiCall1,
apiCall2,
).pipe(
tap(([apiCall1Res, apiCall2Res]) => { r1 = apiCall1Res; r2 = apiCall2Res;}),
map(([apiCall1Res, apiCall2Res]) => apiCall1Res.id), // adjust flow to apiCall3
switchMap((apiCall1ResId) => of(apiCall1ResId).pipe(map(id => id+2))), // <-- apiCall3 mock on the fly
map(apiCall3Res => [r1, r2, apiCall3Res])
);
source.subscribe(console.log);
As you can check on the output you receive the 3 values at the subscription observer code.

rxjs: access the result of original observable after it was switchMap'ped

How do I access the resultB in the tap operator after it was switchMapped ?
streamA$.pipe(
switchMap(resultA => {
const streamB$ = resultA ? streamB1$ : streamB2$;
return streamB$.pipe( // <- nesting
switchMap(resultB => loadData(resultB)),
tap(data => {
// how do I access resultB here?
})
);
})
);
bonus question:
Is it possible to avoid the nesting here, and chain the whole flow under single pipe?
Please consider the following example:
streamA$.pipe(
switchMap(resultA => resultA ? streamB1$ : streamB2$),
switchMap(resultB => loadData(resultB).pipe(map(x => [resultB, x]))),
tap([resultB, data] => {})
);
This is how you can write your observable to get access of resultB and flat the observable operators chain -
streamA$.pipe(
switchMap(resultA => iif(() => resultA ? streamB1$ : streamB2$),
switchMap(resultB => forkJoin([loadData(resultB), of(resultB)])),
tap(([loadDataResponse, resultB]) => {
//loadDataResponse - This will have response of observable returned by loadData(resultB) method
//resultB - This is resultB
})
);
The basic problem here is that switchMap is being used to transform values to emit a certain shape into the stream. If your resultB value isn't part of that shape, then operators further down the chain won't have access to it, because they only receive the emitted shape.
So, there are basically 2 options:
pass an intermediate shape, that contains your value
use a nested pipe to bring both pieces of data into the same operator scope
The solutions suggested so far involve mapping to an intermediate object. My preference is to use the nested pipe, so the data flowing through the stream is a meaningful shape. But, it really comes down to preference.
Using the nested pipe, your code would look something like this:
streamA$.pipe(
switchMap(resultA => {
const streamB$ = resultA ? streamB1$ : streamB2$;
return streamB$.pipe(
switchMap(resultB => loadData(resultB).pipe(
tap(data => {
// you can access resultB here
})
))
);
})
);
Note: you can use iif to conditionally choose a source stream:
streamA$.pipe(
switchMap(resultA => iif(()=>resultA, streamB1$, streamB2$).pipe(
switchMap(resultB => loadData(resultB).pipe(
tap(data => {
// you can access resultB here
})
))
))
);
It can be helpful to break out some of the logic into separate functions:
streamA$.pipe(
switchMap(resultA => doSomeWork(resultA)),
miscOperator1(...),
miscOperator2(...)
);
doSomeWork(result) {
return iif(()=>result, streamB1$, streamB2$).pipe(
switchMap(resultB => loadData(resultB).pipe(
tap(data => {
// you can access resultB here
})
))
))
}

Splitting long piped chains in sub-chains without loosing information about parameters passes between sub-chains

I have a long chain of operations within a pipe. Sub-parts of this chain represent some sort of high level operation. So, for instance, the code could look something like
firstObservable().pipe(
// FIRST high level operation
map(param_1_1 => doStuff_1_1(param_1_1)),
concatMap(param_1_2 => doStuff_1_2(param_1_2)),
concatMap(param_1_3 => doStuff_1_3(param_1_3)),
// SECOND high level operation
map(param_2_1 => doStuff_2_1(param_2_1)),
concatMap(param_2_2 => doStuff_2_2(param_2_2)),
concatMap(param_2_3 => doStuff_2_3(param_2_3)),
)
To improve readability of the code, I can refactor the example above as follows
firstObservable().pipe(
performFirstOperation(),
performSecondOperation(),
}
performFirstOperation() {
return pipe(
map(param_1_1 => doStuff_1_1(param_1_1)),
concatMap(param_1_2 => doStuff_1_2(param_1_2)),
concatMap(param_1_3 => doStuff_1_3(param_1_3)),
)
}
performSecondOperation() {
return pipe(
map(param_2_1 => doStuff_2_1(param_2_1)),
concatMap(param_2_2 => doStuff_2_2(param_2_2)),
concatMap(param_2_3 => doStuff_2_3(param_2_3)),
)
}
Now, the whole thing works and I personally find the code in the second version more readable. What I loose though is the information that performFirstOperation() returns a parameter, param_2_1, which is then used by performSecondOperation().
Is there any different strategy to break a long pipe chain without actually loosing the information of the parameters passed from sub-pipe to sub-pipe?
setting aside the improper usage of forkJoin here, if you want to preserve that data, you should set things up a little differently:
firstObservable().pipe(
map(param_1_1 => doStuff_1_1(param_1_1)),
swtichMap(param_1_2 => doStuff_1_2(param_1_2)),
// forkJoin(param_1_3 => doStuff_1_3(param_1_3)), this isn't an operator
concatMap(param_2_1 => {
const param_2_2 = doStuff_2_1(param_2_1); // run this sync operation inside
return doStuff_2_2(param_2_2).pipe(
concatMap(param_2_3 => doStuff_2_3(param_2_3)),
map(param_2_4 => ([param_2_1, param_2_4])) // add inner map to gather data
);
})
)
this way you've built your second pipeline inside of your higher order operator, so that you can preserve the data from the first set of operations, and gather it with an inner map once the second set of operations has concluded.
for readability concerns, you could do something like what you had:
firstObservable().pipe(
performFirstOperation(),
performSecondOperation(),
}
performFirstOperation() {
return pipe(
map(param_1_1 => doStuff_1_1(param_1_1)),
swtichMap(param_1_2 => doStuff_1_2(param_1_2)),
// forkJoin(param_1_3 => doStuff_1_3(param_1_3)), this isn't an operator
)
}
performSecondOperation() {
return pipe(
concatMap(param_2_1 => {
const param_2_2 = doStuff_2_1(param_2_1);
return doStuff_2_2(param_2_2).pipe(
concatMap(param_2_3 => doStuff_2_3(param_2_3)),
map(param_2_4 => ([param_2_1, param_2_4]))
);
})
)
}
an alternative solution would involve multiple subscribers:
const pipe1$ = firstObservable().pipe(
performFirstOperation(),
share() // don't repeat this part for all subscribers
);
const pipe2$ = pipe1$.pipe(performSecondOperation());
then you could subscribe to each pipeline independently.
I broke one complex operation into two like this:
Main Code
dataForUser$ = this.userSelectedAction$
.pipe(
// Handle the case of no selection
filter(userName => Boolean(userName)),
// Get the user given the user name
switchMap(userName =>
this.performFirstOperation(userName)
.pipe(
switchMap(user => this.performSecondOperation(user))
))
);
First Operation
// Maps the data to the desired format
performFirstOperation(userName: string): Observable<User> {
return this.http.get<User[]>(`${this.userUrl}?username=${userName}`)
.pipe(
// The query returns an array of users, we only want the first one
map(users => users[0])
);
}
Second Operation
// Merges with the other two streams
performSecondOperation(user: User) {
return forkJoin([
this.http.get<ToDo[]>(`${this.todoUrl}?userId=${user.id}`),
this.http.get<Post[]>(`${this.postUrl}?userId=${user.id}`)
])
.pipe(
// Map the data into the desired format for display
map(([todos, posts]) => ({
name: user.name,
todos: todos,
posts: posts
}) as UserData)
);
}
Notice that I used another operator (switchMap in this case), to pass the value from one operator method to another.
I have a blitz here: https://stackblitz.com/edit/angular-rxjs-passdata-deborahk

RxJs - conditional subscription chain with maps and operators instead of nesting

I'm trying to set up an RxJs recipe to perform some steps and do operations conditionally based on the result of some subscriptions.
The pseudo-code is:
if (trySocialSign() succeeds) {
if (tryGetUserFromDatabase() succeeds) {
do.some.stuff
return
} else {
do.other.stuff
}
}
Right now I have this ugly function that works but I'm wondering if there's a prettier way using pipes and maps and other rxjs operators to be able to achieve the same effect in a more idiomatic way with less nesting. Can someone please help me?
this.auth.getCurrentUserAsync().subscribe(
(u: EasyAuthUser) => {
this.currentUser = u;
this.users.getUser().subscribe(
(user: User) => {
this.onExistingUserSignIn.emit(user);
},
(err: HttpErrorResponse) => {
if (redirected) {
this.onSignIn.emit(u);
}
}
);
},
(err: HttpErrorResponse) => {this.handleHttpError(err)}
)
You could do it like the following code (I didn't test it for obvious reasons). All side-effects are performed only from let. There's also one catchError that will suppress the inner error.
this.auth.getCurrentUserAsync().pipe(
let((u: EasyAuthUser) => this.currentUser = u),
mergeMap((u: EasyAuthUser) => this.users.getUser().pipe(
let(
(user: User) => this.onExistingUserSignIn.emit(user),
(err: HttpErrorResponse) => {
if (redirected) {
this.onSignIn.emit(u);
}
}
),
catchError(e => empty()), // Maybe you don't even want this `catchError`
))
).subscribe(
user => ...,
(err: HttpErrorResponse) => this.handleHttpError(err),
);

Using subscribe on a GroupedObservable in map

I have an array of objects like the follwing:
private questions: Question[] = [
{
title: "...",
category: "Technologie",
answer: `...`
},
{
title: "...",
category: "Technologie",
answer: `...`
},
{
title: "...",
category: "eID",
answer: `...`
}
];
And I would like to group them by categories, filter them based on a value and return the result as an array. Currently, I'm using this:
Observable
.from(this.questions)
.groupBy(q => q.category)
.map(go =>
{
let category: Category = { title: go.key, questions: [] };
go.subscribe(d => category.questions.push(d));
return category;
})
.filter(c => c.title.toLowerCase().indexOf(value.toLowerCase()) >= 0 || c.questions.filter(q => q.title.toLowerCase().indexOf(value.toLowerCase()) >= 0).length > 0)
.toArray()
This finds the question with the value in the category title but not the one with the value in the question title. I think that's because I'm using a subscribe in map, therefore, the questions are not yet available in the filter method, so I was wondering if there's a possibility to wait for the subscribe to end before going into filter. My research pointed me to flatMap but I can't get it to do what I want.
EDIT
I figured out that I can fix the issue like this:
Observable
.from(this.questions)
.filter(q => q.category.toLowerCase().indexOf(value.toLowerCase()) >= 0 || q.title.toLowerCase().indexOf(value.toLowerCase()) >= 0)
.groupBy(q => q.category)
.map(go =>
{
let category: Category = { title: go.key, questions: [] };
go.subscribe(d => category.questions.push(d));
return category;
})
.toArray()
But I'm still interested in the answer.
When you use groupBy, you get a grouped observable that can be flattened with operators like concatMap, mergeMap, switchMap etc. Within those operators, grouped observables can be transformed separately for each category, i.e. collect the questions together into an array with reduce, and then create the desired object with map.
Observable
.from(questions)
.groupBy(q => q.category)
.mergeMap(go => {
return go.reduce((acc, question) => { acc.push(question); return acc; }, [])
.map(questions => ({ title: go.key, questions }));
})
.filter(c => "...")
.toArray()

Resources