Fetching data from multiple service on single component using RXJS in angular - javascript

I have two services with
ser1.
getdata1() {
this.http.get<{message:string,Data1:any}>('http://localhost:3000/api/1')
.pipe(map((data1)=>{
return Data1.Data1.map(data=>{
return {
id: data._id,
data1Title:data1.data1Title,
}
})
})).subscribe((data1) => {
this.data1=data1
this.serv1Subject.next([...this.data1])
})
}
getData1Listener() {
return this.serv1Subject.asObservable()
}
ser2
getdata2() {
this.http.get<{message:string,Data2:any}>('http://localhost:3000/api/2')
.pipe(map((data2)=>{
return Data2.Data2.map(data=>{
return {
id: data._id,
data2Title:data2.data1Title,
}
})
})).subscribe((data2) => {
this.data2=data2
this.serv2Subject.next([...this.data2])
})
}
getData2Listener() {
return this.serv2Subject.asObservable()
}
Now on componentx i need to fetch the data1 and data2 in nginit and after the data is available need to perform an functionY
How can i use subscribe to trigger an functionY?
In componentx.ts
ngOnInit() {
this.Service1OberableSubject = this.serv1.getData1Listener().subscribe((data1) => {
this.data1 = data1;
})
this.Service2OberableSubject = this.serv2.getData2Listener().subscribe((data2) => {
this.data2 = data2;
})
this.serv1.getdata1()
this.serv2.getdata2()
}

Possibly something like this. Use tap to tap into the response and do anything needed.
And forkJoin will merge the response and give it in an array, the first index will the be response of the first observable that was passed in.
getdata1() {
this.http.get<{message:string,Data1:any}>('http://localhost:3000/api/1').pipe(
map((data1)=>{
return Data1.Data1.map(data=>{
return {
id: data._id,
data1Title:data1.data1Title,
}
})
}),
tap((data1)=>{
//save to data1 if needed
this.serv1Subject.next([...this.data1]))
})
)
}
getdata2() {
this.http.get<{message:string,Data2:any}>('http://localhost:3000/api/2').pipe(
map((data2)=>{
return Data2.Data2.map(data=>{
return {
id: data._id,
data2Title:data2.data1Title,
}
})
}),
tap((data2)=>{
//save to data2 if needed
this.serv2Subject.next([...this.data2]))
})
)
}
forkJoin(
getdata1(),
getdata2()
).subscribe((x:any[])=>this.functionY(x));
functionY([a, b]){
console.log({a: a,b: b});
}

You can use forkJoin from rxjs.
import { forkJoin } from 'rxjs';
component.ts
ngOnInit() {
forkJoin([this.serv1.getData1Listener(), this.serv2.getData2Listener()]).subscribe(data => {
this.data1 = data[0];
this.data2 = data[1];
});
}

Here's an example of how you can have if implemented:
import { zip } from 'rxjs';
import { first } from 'rxjs/operators';
ngOnInit() {
this.serv1.getdata1()
this.serv2.getdata2()
zip(this.serv1.getData1Listener(), this.serv2.getData2Listener())
.pipe(first())
.subscribe(([data1, data2]) => {
this.data1 = data1;
this.data2 = data2;
functionY(data1, data2)
})
}

forkJoin should be the best operator in this scenario. It is equally important to understand other higher order operators, this will help to know when to use those.
For e.g.
concatMap — helps to map observables in sequence while waiting for previous observable to complete. The best use case would be a sequential form save.
mergeMap — helps mapping of observables running in parallel. All the previous Observables are kept alive.
switchMap — switchMap is the way to go if we want to cancel previous subscriptions. Type-ahead feature is best implemented using switchMap combined with deboundTime() and distinctUntilChanged().
exhaustMap — used for ignoring newly emitted values till ongoing observable is complete. Best used when we are saving data on click of a button, which has a probability of being clicked multiple times.

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.

subscribing two observables i.e. observable inside another observable HTTP

I am new to angular. I am trying to post and get data at a time. I am using map and subscribing the observable while the outer observable is subscribed in component.
add(data) {
return this.postModel(`pathHere`, data)
.pipe(map((resp) => {
if(resp.models){
return this.getRoomById(resp.models.id).subscribe();
}
}));
}
I guess you need to use flatMap instead of map as
add(data) {
return this.postModel(`pathHere`, data)
.pipe(flatMap((resp) => {
if(resp.models){
return this.getRoomById(resp.models.id);
}
}));
}
add(data) {
return this.postModel(`pathHere`, data)
.pipe(
switchMap((resp) => {
if (resp.models) {
return this.getRoomById(resp.models.id);
}
return of(resp.models);
})
);
}
You can use switchMap to emit another Observable.
As you used if statement for checking resp.models, you have to handle the case when resp.models is not truthy.

Get observable return value without subscribing in calling class

In TypeScript / Angular, you would usually call a function that returns an observable and subscribe to it in a component like this:
this.productsService.getProduct().subscribe((product) => { this.product = product });
This is fine when the code runs in a class that manages data, but in my opinion this should not be handled in the component. I may be wrong but i think the job of a component should be to ask for and display data without handling how the it is retrieved.
In the angular template you can do this to subscribe to and display the result of an observable:
<h1>{{ product.title | async }}</h1>
Is it possible to have something like this in the component class? My component displays a form and checks if a date is valid after input. Submitting the form is blocked until the value is valid and i want to keep all the logic behind it in the service which should subscribe to the AJAX call, the component only checks if it got a valid date.
class FormComponent {
datechangeCallback(date) {
this.dateIsValid$ = this.dateService.checkDate(date);
}
submit() {
if (this.dateIsValid$ === true) {
// handle form submission...
}
}
}
You can convert rxjs Observables to ES6 Promises and then use the async-await syntax to get the data without observable subscription.
Service:
export class DateService {
constructor(private http: HttpClient) {}
async isDateValid(date): Promise<boolean> {
let data = await this.http.post(url, date, httpOptions).toPromise();
let isValid: boolean;
// perform your validation and logic below and store the result in isValid variable
return isValid;
}
}
Component:
class FormComponent {
async datechangeCallback(date) {
this.dateIsValid = await this.dateService.isDateValid(date);
}
submit() {
if (this.dateIsValid) {
// handle form submission...
}
}
}
P.S:
If this is a simple HTTP request, which completes on receiving one value, then using Promises won't hurt. But if this obersvable produces some continuous stream of values, then using Promises isn't the best solution and you have to revert back to rxjs observables.
The cleanest way IMHO, using 7.4.0 < RxJS < 8
import { of, from, tap, firstValueFrom } from 'rxjs';
const asyncFoo = () => {
return from(
firstValueFrom(
of('World').pipe(
tap((foo) => {
console.info(foo);
})
)
)
);
};
asyncFoo();
// Outputs "World" once
asyncFoo().subscribe((foo) => console.info(foo));
// Outputs "World" twice
The "more cleanest" way would be having a factory (in some service) to build these optionally subscribeable function returns...
Something like this:
const buildObs = (obs) => {
return from(firstValueFrom(obs));
};
const asyncFoo = () => {
return buildObs(
of('World').pipe(
tap((foo) => {
console.info(foo);
})
)
);
};

how to conditionally subscribe to another observable on success of first observable?

I m tring to use concatmap and complete the observables sequentially when queryParams has id it should only call getbearer tokens or else it should navigate to error with securekey but in reality its going to error page with bearer-token
const result = this.activatedRoute.queryParams.pipe(
concatMap((params) => {
if (params.hasOwnProperty('id')) {
sessionStorage.setItem('secureKey', params.id);
return this.httpServiceService.getBearerTokens();
} else {
this.router.navigate(['error'], {
queryParams: { 'error-key': 'secure-key' },
});
}
})
);
result.subscribe(
(response: any) => {
if (response.access_token) {
sessionStorage.setItem('bearerToken', response.access_token);
this.router.navigate(['summary']);
} else {
this.router.navigate(['error'], {
queryParams: { 'error-key': 'bearer-token' },
});
}
},
(error) => {
this.router.navigate(['error'], {
queryParams: { 'error-key': 'bearer-token' },
});
}
);
I think concatMap is just asking for a race condition (I don't know how often and how easy the query parameters change, though). I would use switchMap instead, because I imagine that the moment we navigate from "?id=alice" to "?id=bob", the matter of Alice's token becomes immaterial.
Generally, conditionally subscribing to another stream should work just fine with switchMap and filter:
firstStream$.pipe(
filter(result => someCondition(result)),
switchMap(result => getAnotherStream(result))
).subscribe(//...
Oh, and by the way, your concatMap returns a stream only if (params.hasOwnProperty('id')). Does that even compile? Return an observable in the else branch as well, it could even be EMPTY, but probably should be throwError.

How to call a function after the completion of two Angular 2 subscriptions?

I am on Angular 2.3.1 and I am fairly new to both Angular and event based programming. I have two subscriptions, route.params.subscribe and engineService.getEngines(). In my onInit I want to call getEngineName after this.route.params.subscribe and this.engineService.getEngines().subscribe complete.
Reason for this: getEngineName functions depends on the engineId from the queryParams and the engines array which is populated after the completion of getEngines() call.
I did look at flatMap and switchMap but I did not completely understand them.
This is the code in the component:
export class ItemListComponent implements OnInit {
items: Item[];
engines: Engine[];
private engineId: number;
constructor(
private router: Router,
private route: ActivatedRoute,
private itemService: ItemService,
private engineService: EngineService
) {}
ngOnInit() {
this.route.params.subscribe((params: Params) => {
this.engineId = +params['engineId'];
// itemService is irrelevant to this question
this.itemService.getItems({engineId: this.engineId})
.subscribe((items: Item[]) => {
this.items = items;
});
});
this.engineService.getEngines()
.subscribe(engines => this.engines = engines);
// This should only run after engineId and engines array have been populated.
this.getEngineName(this.engineId);
}
getEngineName(engineId: number) {
this.engines.find((engine) => {
return engine.id === engineId;
})
}
}
Why don't you just move the logic inside the route.params callback?
this.route.params.subscribe((params: Params) => {
this.engineId = +params['engineId'];
// itemService is irrelevant to this question
this.itemService.getItems({engineId: this.engineId})
.subscribe((items: Item[]) => {
this.items = items;
});
//this.engineId is defined here (1)
this.engineService.getEngines()
.subscribe((engines) => {
this.engines = engines;
//this.engines is defined here (2)
this.getEngineName(this.engineId);
});
});
with flatMap and forkJoin:
this.route.params.flatMap((params: Params) => {
this.engineId = +params['engineId'];
return Observable.forkJoin(
this.itemService.getItems({engineId: this.engineId}),
this.engineService.getEngines()
)
}).subscribe((data)=>{
let items = data[0];
let engines = data[1];
this.items = items;
this.engines = engines;
this.getEngineName(this.engineId);
});
switchMap is recommended in this scenario.
this.route.params.pluck('engineId') //pluck will select engineId key from params
.switchMap(engineId => {
this.getItems(engineId);
return this.engineService.getEngines().map(engines => {
/*this.engineService.getEngines() emits the list of engines.
Then use map operator to convert the list of engines to engine we are looking for
*/
return engines.find((engine) => {
return engine.id === engineId;
})
})
}).subscribe(engine => {
//engine
})
getItems(engineId) {
this.itemService.getItems({engineId: engineId})
.subscribe((items: Item[]) => {
this.items = items;
});
}
Suppose the engineId in the params changes, the first observable this.route.params.pluck('engineId') will emit data, which will cause the next observable this.engineService.getEngines() to get fired. Now suppose the route changes before this observable emits the data. Here you need to cancel getEngines observable to prevent error. This is done by switchMap.
switchMap cancels inner observable if outer observable is fired.
PS: I have avoided keeping any states like this.engineId etc.

Categories

Resources