How do I make sure one Subcription finishes before another? - javascript

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.

Related

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$)
)
)

Whats the correct method to chain Observable<void> with typescript (Angular)?

I've been searching for the right/best/correct way to chain a few Observable methods. I've read on the using pipe() and map() and so on, but i'm not 100% i fully understand. The basis is i have a few actions to carry out, which some needs to be in sequence, and some dont.
Below are the 4 methods i need to call.
createOrder(order:Order):Observable<void>{
return new Observable((obs)=>
//Do Something
obs.complete();
)
}
updateCurrentStock(order:Order):Observable<void>{
return new Observable((obs)=>
//Do Something
obs.complete();
)
}
updateSalesSummary(order:Order):Observable<void>{
return new Observable((obs)=>
//Do Something
obs.complete();
)
}
updateAnotherDocument(order:Order):Observable<void>{
return new Observable((obs)=>
//Do Something
obs.complete();
)
}
From this 4, the flow should be createOrder ->updateCurrentStock->updateSalesSummary, updateAnotherDocument.
As of now, what i have is
var tasks = pipe(
flatMap(e => this.createOrder(order)),
flatMap(e => this.updateCurrentStock(order)),
flatMap(e => forkJoin([this.updateSalesSummary(order),this.updateAnotherDocument(order)])),
);
of(undefined).pipe(tasks).subscribe({
error: (err) => {
console.log(err);
obs.error(err);
},
complete: () => {
console.log('completed');
obs.complete();
}
});
It works, but i'm not sure if this is the right/cleanest way of doing it and if there is any possible issues in the future.
Using concat
concat will subscribe to your streams in order (not starting the next until the previous one completes.
This should be roughly equivalent.
One difference here is that unlike mergeMap, you're not transforming the output of an api call, it still gets emitted. Since you're not doing anything with the next callback in your subscription, it'll still look similar in the case.
concat(
this.createOrder(order),
this.updateCurrentStock(order),
forkJoin([
this.updateSalesSummary(order),
this.updateAnotherDocument(order)
])
).subscribe({
error: concosle.log,
complete: () => console.log('completed');
});
An Aside:
Here's how I would re-write your original code to be a bit easier to read.
this.createOrder(order).pipe(
mergeMap(_ => this.updateCurrentStock(order)),
mergeMap(_ => forkJoin([
this.updateSalesSummary(order),
this.updateAnotherDocument(order)
]),
).subscribe({
error: (err) => {
console.log(err);
obs.error(err); // <-- What's obs here?
},
complete: () => {
console.log('completed');
obs.complete();
}
});
There are a lot of rxjs operators, I recommend you read https://www.learnrxjs.io/learn-rxjs.
When you have an observable that depends on other's value use switchMap
Example:
const userId$: Observable<number>;
function getUserData$(userId: number): Observable<Data> {
// yourService.fetchUser(userId);
}
userId$.pipe(switchMap((userId: number) => getUserData$(userId)));
When you do not care about the order, you can use:
if you want to emit the last value when all observables complete: forkJoin
if you want to emit every value as any observable emits a value: combineLatest
Example:
const userId$: Observable<number>;
function getUserAge$(userId: number): Observable<number> {
// ...
}
function getUserName$(userId: number): Observable<String> {
// ...
}
userId$.pipe(
switchMap((userId: number) =>
forkJoin([getUserAge$(userId), getUserName$(userId)])
)
);
In your case I think the order does not matter, as none of your observables needs data from the previous one. So I think you should use combineLatest.
If the order of emission and subscription of inner observables is important, try concatMap.

Assign observable within callback in Angular 9

I am completely a newbie to Angular. Some how struggled to get the initial code working now I am stuck here.
Use case : Show loading bar. Fetch merchant list to an observable. Stop loading bar.
Problem:
fetchUsers($event) {
let searchTerm = $event.term;
if (searchTerm.length > 3) {
this.isLoading=true;
this.people$ = null;
this.userService.getMerchantsBySearch(searchTerm);
--> this.isLoading=false;
}
}
this.isLoading becomes false even before getting the response from getMerchantsBySearch call. I know I will need to perform this step in callback but I am not sure how to.
I want to do something like this but I a missing something. What is the correct way to do it.
fetchUsers($event) {
let searchTerm = $event.term;
if (searchTerm.length > 3) {
this.isLoading=true;
this.people$ = null;
this.userService.getMerchantsBySearch(searchTerm).subscribe(
res={
---> this.people$ =res;
---> this.isLoading=false;
}
);
}
}
dashboard.component.ts
people$: Observable<IMerchant[]>;
fetchUsers($event) {
let searchTerm = $event.term;
if (searchTerm.length > 3) {
this.isLoading=true;
this.people$ = null;
this.userService.getMerchantsBySearch(searchTerm);
this.isLoading=false;
}
}
user.service.ts
getMerchantProfile(id){
var options = this.getOptions();
return this.http.get<IMerchant[]>(environment.APIBaseURL+"/getMerchantProfile/"+id,options);
}
merchant.model.ts
export interface IMerchant{
email:String,
mobile : String,
password: String,
businessName : String,
otp:Number,
info:string,
url:String
}
i prefer using the teardown method .add(). it doesn't pollute your pipe chain of data manipulation and is also executed after an error is handled:
this.userService.getMerchantsBySearch(searchTerm)
.pipe(
// manipulate data here
)
.subscribe(
res => {
// handle final data
},
error => {
// handle error
}
).add(() => {
// teardown here (e.g. this.isLoading=false;)
});
By the way: if this.people$ is an observable, you can not assign it inside the subscribe callback. you'll need to assign it to the response of the userService method you are using. Be careful not to call subscribe on the stream since it will return you a subscription instead of an observable:
this.people$ = this.userService.getMerchantsBySearch(searchTerm).pipe(...)
You can actually pipe it and add a finalize
this.userService.getMerchantsBySearch(searchTerm)
.pipe(finalize(() => this.isLoading=false))
.subscribe(
res => {
this.people$ =res;
});

Angular RXJS retry a task based on a value

Ok, so I have a service that checks to see if a particular 3rd party JS plugin has loaded. I want to listen in for when it has entered the DOM, meanwhile it is in an undefined state. How do I do that? So far I have tried using a subject and retrying periodically but I can't get it to work:
$apiReady: Subject<boolean> = new Subject();
RegisterOnNewDocumentLoadedOnDocuViewareAPIReady(reControl: any): any {
this.$apiReady.asObservable().subscribe((isReady: boolean) => {
if (isReady) {
//Do something
return of(isReady);
}
})
let IsreControlInitialized = ThirdPartyAPI.IsInitialized(reControl);
if (IsreControlInitialized) {
this.$apiReady.next(true);
}
return throwError(false);
}
Then in the component:
this._apiService.RegisterOnAPIReady(this.elementID).pipe(
retryWhen(error => {
return error.pipe(delay(2000)); //<---- Doesn't work
})).subscribe((response: boolean) => {
if (response) {
//Do some stuff
}
});
My intentions were to check if the API element had been loaded, if not retry in 2 seconds but this doesn't work, can anyone help?
Throwing and catching an error until some condition is met is a little counter-intuitive to me.
My approach consists of using the interval operator along with takeUntil operator.
apiReady = new Subject();
interval(2000) // Check every 2s
.pipe(
map(() => this.isApiReady())
takeUntil(this.apiReady)
)
.subscribe(isReady => {
if (isReady) {
this.apiReady.next();
this.apiReady.complete();
}
})

use data outside of .subscribe in typescript file

I am very new to typescript/ionic 4. I am trying to access data stored in firebase and use it in my typescript file. when in .subscribe I can display the data as requested. but this is not what I am looking for. I need to perform the calculation outside of .subscribe on my page.ts .
I have seen many similar issues, but I cannot seem to get a solution.
Here is my Typescript services file
export interface Place{
title: string;
type: string;
latitude: number;
longitude: number;
}
export class PlaceService {
placess: Place[];
place: Place;
private placesCollection: AngularFirestoreCollection<Place>;
private places: Observable<Place[]>;
constructor(db: AngularFirestore) {
this.placesCollection = db.collection<Place>('places');
this.places = this.placesCollection.snapshotChanges().pipe(
map(actions =>{
return actions.map(a => {
const data = a.payload.doc.data();
const id = a.payload.doc.id;
return{ id, ...data};
});
})
);
}
getPlaces() {
return this.places;
}
}
and the relevant part in my page typescript
import { PlaceService, Place } from '../services/place.service';
places: Place[];
ngOnInit() {
this.placeService.getPlaces()
.subscribe(res =>{
this.places = res;
console.log(this.places[0].title);//WORKS
});
console.log(this.places[0].title);//FAILED
}
I get the following error message:
MapPage_Host.ngfactory.js? [sm]:1 ERROR TypeError: Cannot read property '0' of undefined
Your problem is that your code works as you wrote it. When the page initializes the ngOnInit is called. Inside the code goes to the first element (this.placeService.getPlaces() ... ) and immediately goes to the seconde element (console.log(this.places[0]). This throws the error, because the places variable is not yet set from your call to the placeService and is currently undefined.
ngOnInit() {
this.placeService.getPlaces() // called first
.subscribe(res =>{
this.places = res;
console.log(this.places[0].title);
});
console.log(this.places[0].title); // called second (undefined error)
}
If you call a function after you set the places variable, the second console.log() will work.
ngOnInit() {
this.placeService.getPlaces()
.subscribe(res =>{
this.places = res;
console.log(this.places[0].title);
this.showFirstTitle(); // this.places is set
});
}
showFirstTitle() {
console.log(this.places[0].title); // will work
}
.subscribe method has to complete( ajax request has to be 200-OK), inside subscribe method you can store into your local variables, Then further modifications are possible.
you can not use a variable which has no data.
this.placeService.getPlaces()
.subscribe(res =>{
this.places = res;
});
will take some seconds to complete the ajax call and fetch the response and storing in "Places".
workaround(not recommended) use set timeout function wait for at least 2 sec. increment seconds until you find a minimal seconds that request and response completed.
then you can do some calculations on this.places.

Categories

Resources