How to subscribe to an observable once? - javascript

My function access() needs to subscribe once, each and every call.
In the snippet below, $valueChanges emits data to each change made. Calling access() without const $ = ... and $.unsubscribe(), $valueChanges observable emits unnecessary streams of values.
Is there an rxjs operator/function that emits once in subscription inside a function? Even if the function is called multiple times the subscription emits once?
access() {
const $ = $valueChanges.pipe(
map((res) =>
...
),
).subscribe((res) => {
...
$.unsubscribe();
});
}

You can consider using the take() operator, and emit only the first value before completing.
According to the documentation, the take operator
Emit provided number of values before completing.
This is how you can use it:
access() {
valueChanges
.pipe(
map((res) =>
...
),
take(1),
).subscribe((res) => {
...
});
}

Try shareReply(1). Then the original stream will be called only once and its emit will be shared with all subscribers. If the stream emits 2nd time - the update will go to all subscribers too.
access() {
const $ = $valueChanges.pipe(
map((res) =>
...
),
// take(1), // in case if you need just 1 emit from it.
shareReply(1), // in case if you don't want to trigger `$valueChanges` on every subscription.
).subscribe((res) => {
...
// $.unsubscribe(); // it's useless in case of `take(1)`.
});
}

Related

How do I make sure one Subcription finishes before another?

globleVariable: any;
ngOnInit() {
// This doesn't work. methodTwo throws error saying "cannot read someField from null. "
this.methodOne();
this.methodTwo();
}
methodOne() {
this.firstService.subscribe((res) => { this.globleVariable = res });
}
methodTwo() {
this.secondService.subscribe((res) => { console.log(this.globleVariable.someField) });
}
As shown above, methodOne set the value of globleVariable and methodTwo uses it, therefore the former must finish running before the latter.
I am wondering how to achieve that.
Instead of subscribing in the methods, combine them into one stream and subscribe to that in ngInit(). You can use tap to perform the side effect of updating globaleVariable that you were previously performing in subscribe().
In the example below the "methods" are converted into fields since there is no reason for them to be methods anymore (you can keep them as methods if you want). Then the concat operator is used to create a single stream, where methodOne$ will execute and then when it's complete, methodTwo$ will execute.
Because concat executes in order, you are guaranteed that globaleVariable will be set by methodOne$ before methodTwo$ begins.
globleVariable: any;
methodOne$ = this.someService.pipe(tap((res) => this.globleVariable = res));
methodTwo$ = this.someService.pipe(tap((res) => console.log(this.globleVariable.someField));
ngOnInit() {
concat(this.methodOne$, this.methodTwo$).subscribe();
}
You can create a subject for which observable 2 will wait to subscribe like below :-
globalVariable: any;
subject: Subject = new Subject();
methodOne() {
this.someService.subscribe((res) => { this.globleVariable = res; this.subject.next(); });
}
methodTwo() {
this.subject.pipe(take(1), mergeMap(() => this.someService)).subscribe((res) => {
console.log(this.globleVariable.someField) });
}
The only way to guarantee a method call after a subscription yields is to use the subscription callbacks.
Subscriptions have two main callbacks a success and a failure.
So the way to implement a method call after the subscription yeilds is to chain it like this:
globleVariable: any;
ngOnInit() {
this.methodOne();
}
methodOne() {
this.someService.subscribe((res) => {
this.globleVariable = res
this.methodTwo(); // <-- here, in the callback
});
}
methodTwo() {
this.someService.subscribe((res) => { console.log(this.globleVariable.someField) });
}
You might want to chain the calls with some other rxjs operators for a more standard usage.
ngOnInit() {
this.someService.method1.pipe(
take(1),
tap(res1 => this.globleVariable = res1)
switchmap(res1 => this.someService.method2), // <-- when first service call yelds success
catchError(err => { // <-- failure callback
console.log(err);
return throwError(err)
}),
).subscribe(res2 => { // <-- when second service call yelds success
console.log(this.globleVariable.someField) });
});
}
Please remember to complete any subscriptions when the component is destroyed to avoid the common memory leak.
my take,
so it's a bit confusing when you use same service that throws different results, so instead of someService I used firstService and secondService here.
this.firstService.pipe(
switchMap(globalVariable) =>
this.secondService.pipe(
map(fields => Object.assign({}, globalVariable, { someField: fields }))
)
)
).subscribe(result => {
this.globalVariable = result;
})
What I like about this approach is that you have the flexibility on how you want to use the final result as it is decoupled with any of the property in your class.

Refactoring chained RxJs subscriptions

I have a piece of code that I need to refactor because it's a hell of chained subscriptions.
ngOnInit(): void {
this.dossierService.getIdTree()
.subscribe(idTree => {
this.bootstrappingService.refreshObligations(idTree)
.subscribe(() => {
this.dossierPersonsService.retrieveDossierPersons(idTree)
.subscribe(debtors => {
this.retrieveObligations();
this.debtors = debtors;
});
});
});
}
The first call dossierService.getIdTree() retrieves idTree which is used by other services except obligationsService.retrieveObligations().
All service methods should be executed in the order they executed now. But retrieveDossierPersons and retrieveObligations can be executed in parallel.
retrieveObligations() is a method that subscribes to another observable. This method is used in a few other methods.
I've refactored it and it seems to work. But did I refactor it in a proper way or my code can be improved?
this.dossierService.getIdTree()
.pipe(
map(idTree => {
this.idTree = idTree;
}),
switchMap(() => {
return this.bootstrappingService.refreshObligations(this.idTree)
}),
switchMap(
() => {
return this.dossierPersonsService.retrieveDossierPersons(this.idTree)
},
)
)
.subscribe(debtors => {
this.retrieveObligations();
this.debtors = debtors;
});
Something like this (not syntax checked):
ngOnInit(): void {
this.dossierService.getIdTree().pipe(
switchMap(idTree =>
this.bootstrappingService.refreshObligations(idTree)).pipe(
switchMap(() => this.dossierPersonsService.retrieveDossierPersons(idTree).pipe(
tap(debtors => this.debtors = debtors)
)),
switchMap(() => this.retrieveObligations())
)
).subscribe();
}
Using a higher-order mapping operator (switchMap in this case) will ensure that the inner observables are subscribed and unsubscribed.
In this example, you don't need to separately store idTree because you have access to it down the chained pipes.
You could try something like:
ngOnInit(): void {
const getIdTree$ = () => this.dossierService.getIdTree();
const getObligations = idTree => this.bootstrappingService.refreshObligations(idTree);
const getDossierPersons = idTree => this.dossierPersonsService.retrieveDossierPersons(idTree);
getIdTree$().pipe(
switchMap(idTree => forkJoin({
obligations: getObligations(idTree)
debtors: getDossierPersons(idTree),
}))
).subscribe(({obligations, debtors}) => {
// this.retrieveObligations(); // seems like duplicate of refreshObligations?
this.debtors = debtors;
});
}
Depending on the rest of the code and on the template, you might also want to avoid unwrapping debtors by employing the async pipe instead
forkJoin will only complete when all of its streams have completed.
You might want also want to employ some error handling by piping catchError to each inner observable.
Instead of forkJoin you might want to use mergeMap or concatMap (they take an array rather than an object) - this depends a lot on logic and the UI. concatMap will preserve the sequence, mergeMap will not - in both cases, data could be display accumulatively, as it arrives. With forkJoin when one request gets stuck, the whole stream will get stuck, so you won't be able to display anything until all streams have completed.
You can use switchMap or the best choice is concatMap to ensure orders of executions
obs1$.pipe(
switchMap(data1 => obs2$.pipe(
switchMap(data2 => obs3$)
)
)

switchMap distincted by property in RxJs

Let's say, I have a stream of actions. Each action is assigned some id. Like this:
const actions$ = of({ id: 1 }, { id: 2 }, { id: 1 });
Now, for each action, I want to perform some logic in switchMap:
actions$.pipe(switchMap(a => /* some cancellable logic */)).subscribe(...);
The problem is that each emitted action cancels previous 'some cancellable logic'.
Is it possible to cancel 'some cancellable logic' based on action id, preferably an operator? Something like:
actions$.pipe(switchMapBy('id', a => /*some cancellable logic */)).subscribe(...)
Essentially, current behaviour with switchMap:
1. actions$ emits id #1. switchMap subscribes to nested observable.
2. actions$ emits id #2. switchMap unsubscribes from previous nested observable. Subscribes to new one.
3. actions$ emits id #1. switchMap again unsubscribes from previous nested observable. Subscribes to new one.
Expected behaviour:
1. actions$ emits id #1. switchMap subscribes to nested observable.
2. actions$ emits id #2. switchMap again subscribes to nested observable (this time with #2). And here's the difference, it doesn't cancel the one from #1.
3. actions$ emits id #1. switchMap unsubscribes from nested observable for #1. Subscribes again, for #1.
this seems to be a use case for the mergeMap operator. The use case of switchMap is to only maintain one inner subscription and cancel previous ones, which is not what you're after here. You want multiple inner subscriptions and you want them to cancel when a new value of the same id comes through, so implement some custom logic to do that.
something along the lines of:
action$.pipe(
mergeMap(val => {
return (/* your transform logic here */)
.pipe(takeUntil(action$.pipe(filter(a => a.id === val.id)))); // cancel it when the same id comes back through, put this operator at the correct point in the chain
})
)
you can turn this into something resuable by writing a custom operator:
import { OperatorFunction, Observable, from } from 'rxjs';
import { takeUntil, filter, mergeMap } from 'rxjs/operators';
export function switchMapBy<T, R>(
key: keyof T,
mapFn: (val: T) => Observable<R> | Promise<R>
): OperatorFunction<T, R> {
return input$ => input$.pipe(
mergeMap(val =>
from(mapFn(val)).pipe(
takeUntil(input$.pipe(filter(i => i[key] === val[key])))
)
)
);
}
and use it like:
action$.pipe(
switchMapBy('id', (val) => /* your transform logic here */)
);
here's a blitz of it in action: https://stackblitz.com/edit/rxjs-x1g4vc?file=index.ts
use filter operation before switchMap to exclude canceled ids, like this
of({ id: 1 }, { id: 2 }, { id: 1 }).pipe(
filter(id => ![1,2].includes(id)), // to exclude ids (1,2)
switchMap(id => /*some cancellable logic */ )
).subscribe(...)

RxJS takeUntil doesn't unsubscribe

I want to unsubscribe in declarative style with "takeUntil" operator. But that's basically does't work. I can see console output anyway.
const unsubscribe = new Subject();
function printFoo() {
of('foo')
.pipe(takeUntil(unsubscribe))
.subscribe(console.log) // Why I can see 'foo' in the console?
}
function onDestroy() {
unsubscribe.next();
unsubscribe.complete();
}
onDestroy()
setTimeout(() => printFoo(), 200)
StaackBlitz:
https://stackblitz.com/edit/rxjs-svfkxg?file=index.ts
P.S. I expected that even unsubscribe.next() would be enough to unsubscribe, but even with unsubscribe.complete() it doesn't work.
You're calling onDestroy() before the chain with takeUntil is even created.
When you eventually call printFoo() the previous emissions to unsubscribe won't be re-emited and the Subject unsubscribe is already completed anyway so takeUntil in this case will never complete the chain.
Because the Subject emits before printFoo subscription.
After you subscribe there are no more Subject emittions.
You could use BehaviorSubject instead, since it holds the emitted values (the last
emitted value):
const unsubscribe = new BehaviorSubject(false);
function printFoo() {
of('foo')
.pipe(takeUntil(unsubscribe.pipe(filter(value => !!value)))) // Don't unsub if it's false emitted
.subscribe(console.log)
}
function onDestroy() {
unsubscribe2.next(true); // Emit true to cancel subscription
}

Subscribe onComplete never completes with flatMap

I'm using Angular 6 with RxJS 6.2.2 and RxJS Compact 6.2.2.
I have a code to call my api service to load some records, which is:
this.route.params
.flatMap((params: Params) => {
if (!params['id']) {
return Observable.throwError('Id is not specified.');
}
this.itemId = params['id'];
this.isEditMode = true;
this.loadCategoryGroupCondition = new LoadCategoryGroupViewModel();
this.loadCategoryGroupCondition.id = [this.itemId];
this.loadCategoryGroupCondition.pagination = new Pagination();
return this.categoryGroupService
.loadCategoryGroup(this.loadCategoryGroupCondition);
})
.subscribe(
(loadCategoryGroupResult: SearchResult<CategoryGroup>) => {
console.log(loadCategoryGroupResult);
},
() => {},
() => {
console.log('Completed')
});
The code above can print a list of my items returned from my api service. That means onSuccess has been called.
But the complete method is fired.
What is wrong with my code ?
Thank you,
As discussed, the flatMap operator does itself not complete its source observable. You are using this.route.params as your source observable, which is long-lived - it never completes by itself.
To get a complete notification you can use an operator such as take. It will re-emit the number of items you pass as a parameter and complete afterwards. For example, if you just want to receive the current route and are not interested in further notifications of your source observable, use take(1), like:
this.route.params
.take(1)
.flatMap((params: Params) => {
Also, note that the recommeded way for doing this in RxJS 6+ is using pipeable operators. This would look like so:
this.route.params.pipe(
first(),
mergeMap((params: Params) => {
...
})
I also replaced the operators with the newer recommended variants.

Categories

Resources