How to make recursive HTTP calls using RxJs operators? - javascript

I use the following method in odder to retrieve data by passing pageIndex (1) and pageSize (500) for each HTTP call.
this.demoService.geList(1, 500).subscribe(data => {
this.data = data.items;
});
The response has a property called isMore and I want to modify my method in odder to continue HTTP calls if isMore is true. I also need to merge the returned values and finally return the accumulated values.
For example, assuming that there are 5000 records and until 10th HTTP call, the service returns true for isMore value. After 10th HTTP call, it returns false and then this method sets this.data value with the merged 5000 records. For this problem, should I use mergeMap or expand or another RxJs operator? What is the proper way to solve this problem?
Update: I use the following approach, but it does not merge the returned values and not increase the pageIndex. For this reason it does not work (I tried to make some changes, but could not make it work).
let pageIndex = 0;
this.demoService.geList(pageIndex+1, 500).pipe(
expand((data) => {
if(data.isComplete) {
return of(EMPTY);
} else {
return this.demoService.geList(pageIndex+1, 500);
}
})
).subscribe((data) => {
//your logic here
});
Update II:
of({
isMore : true,
pageIndex: 0,
items: []
}).pipe(
expand(data => demoService.geList(data.pageIndex+1, 100)
.pipe(
map(newData => ({...newData, pageIndex: data.pageIndex+1}))
)),
// takeWhile(data => data.isMore), //when using this, it does not work if the total record is less than 100
takeWhile(data => (data.isMore || data.pageIndex === 1)), // when using this, it causing +1 extra HTTP call unnecessarily
map(data => data.items),
reduce((acc, items) => ([...acc, ...items]))
)
.subscribe(data => {
this.data = data;
});
Update III:
Finally I made it work by modifying Elisseo's approach as shown below. Howeveri **I need to make it void and set this.data parameter in this getData() method. How can I do this?
getData(pageIndex, pageSize) {
return this.demoService.geList(pageIndex, pageSize).pipe(
switchMap((data: any) => {
if (data.isMore) {
return this.getData(pageIndex+1, pageSize).pipe(
map((res: any) => ({ items: [...data.items, ...res.items] }))
);
}
return of(data);
})
);
}
I want to merge the following subscribe part to this approach but I cannot due to some errors e.g. "Property 'pipe' does not exist on type 'void'."
.subscribe((res: any) => {
this.data = res;
});

getData(pageIndex, pageSize) {
return this.demoService.getList(pageIndex, pageSize).pipe(
switchMap((data: any) => {
if (!data.isCompleted) {
return this.getData(pageIndex+1, pageSize).pipe(
map((res: any) => ({ data: [...data.data, ...res.data] }))
);
}
return of(data);
})
);
}
stackblitz
NOTE: I updated pasing as argument pageIndex+1 as #mbojko suggest -before I wrote pageIndex++
UPDATE 2
Using expand operator we need take account that we need feed the "recursive function" with an object with pageIndex -it's necesarry in our call- for this, when we make this.demoService.getList(data.pageIndex+1,10) we need "transform the result" adding a new property "pageIndex". for this we use "map"
getData() {
//see that initial we create "on fly" an object with properties: pageIndex,data and isCompleted
return of({
pageIndex:1,
data:[],
isCompleted:false
}).pipe(
expand((data: any) => {
return this.demoService.getList(data.pageIndex,10).pipe(
//here we use map to create "on fly" and object
map((x:any)=>({
pageIndex:data.pageIndex+1, //<--pageIndex the pageIndex +1
data:[...data.data,...x.data], //<--we concatenate the data using spread operator
isCompleted:x.isCompleted})) //<--isCompleted the value
)
}),
takeWhile((data: any) => !data.isCompleted,true), //<--a take while
//IMPORTANT, use "true" to take account the last call also
map(res=>res.data) //finally is we only want the "data"
//we use map to return only this property
)
}
Well we can do a function like this:
getData() {
of({pageIndex:1,data:[],isCompleted:false}).pipe(
expand((data: any) => {
return this.demoService.getList(data.pageIndex,10).pipe(
tap(x=>{console.log(x)}),
map((x:any)=>({
pageIndex:data.pageIndex+1,
data:[...data.data,...x.data],
isComplete:x.isComplete}))
)
}),
takeWhile((data: any) => !data.isComplete,true), //<--don't forget the ",true"
).subscribe(res=>{
this.data=res.data
})
}
See that in this case we don't return else simple subscribe to the function and equal a variable this.data to res.data -it's the reason we don't need the last map
Update 3 by Mrk Sef
Finally, if you don't want your stream to emit intermittent values and you just want the final concatenated data, you can remove the data concatenation from expand, and use reduce afterward instead.
getData() {
of({
pageIndex: 1,
data: [],
isCompleted: false
})
.pipe(
expand((prevResponse: any) => this.demoService.getList(prevResponse.pageIndex, 10).pipe(
map((nextResponse: any) => ({
...nextResponse,
pageIndex: prevResponse.pageIndex + 1
}))
)
),
takeWhile((response: any) => !response.isCompleted, true),
// Keep concatenting each new array (data.items) until the stream
// completes, then emit them all at once
reduce((acc: any, data: any) => {
return [...acc, ...data.data];
}, [])
)
.subscribe(items => {
this.data=items;
});
}

It doesn't matter if you're total record change as long as api response give you the isMore flag.
I'm skipping the part how to implement reducer action event i'm assuming you've already done that part. So i will just try to explain with pseudo codes.
You have a table or something like that with pagination data. on intial state you can just create an loadModule effect or using this fn:
getPaginationDataWithPageIndex(pageIndex = 1){
this.store.dispatch(new GetPaginationData({ pageIndex: pageIndex, dataSize: 500}));
}
in your GetPaginationData effect
... map(action => {
return apicall.pipe(map((response)=> {
if(response.isMore){
return new updateState({data:response.data, isMore: responseisMore})
} else {
return new updateState({isMore: response.isMore}),
}
}})
`
all you have to left is subscribing store in your .ts if isMore is false you will not display the next page button. and on your nextButton or prevButton's click method you should have to just dispatch the action with pageIndex

I do not think recursion is the correct approach here:
interval(0).pipe(
map(count => this.demoService.getList(count + 1, 500)),
takeWhile(reponse => response.isMore, true),
reduce((acc, curr) => //reduce any way you like),
).subscribe();
This should make calls to your endpoint until the endpoint returns isMore === false. The beautiful thing about interval is that we get the count variable for free.
But if you are set on using recrsion, here is the rxjs-way to do that using the expand-operator (see the docs). I find it slightly less readable, as it requires an if-else-construct which increases code complexity. Also the outer 'counter' variable just isn't optimal.
let index = 1;
this.demoService.geList(index, 500).pipe(
expand(response => response.isMore ? this.demoService.geList(++index, 500) : empty()),
reduce((acc, curr) => //reduce here)
).subscribe();

Related

How to map data correctly before subscription?

I have following function:
this.localStorage.getItem('user').subscribe(user => {
this.user = user;
this.authSrv.getOrders(this.user.einsender).pipe(map(orders => {
map(order => { order["etz"] = "23"; return order})
return orders;
})).subscribe(orders => {
this.orders = orders;
this.completeOrders = orders;
console.log(orders);
this.waitUntilContentLoaded = true;
})
})
The result without the map is:
[{id: 1, etz: "21"}]
With the map from above I try to enter the array, then the order and in the order I try to change the etz property but somehow nothing changes. Can someone look over?
I appreciate any help!
I see multiple issues here.
Try to avoid nested subscriptions. Instead you could use one of the RxJS higher order mapping operators like switchMap. You could find differences b/n different higher order mapping operators here and here.
To adjust each element of the array you need to use Array#map method in addition to the RxJS map operator.
You could use JS spread operator to adjust some of the properties of the object and retain other properties.
Try the following
this.localStorage.getItem('user').pipe(
switchMap(user => {
this.user = user;
return this.authSrv.getOrders(this.user.einsender).pipe(
map(orders => orders.map(order => ({...order, order['etz']: '23'})))
});
})
).subscribe(
orders => {
this.orders = orders;
this.completeOrders = orders;
console.log(orders);
this.waitUntilContentLoaded = true;
},
error => {
// good practice to handle HTTP errors
}
);
map is an operator that goes in a pipe like this:
someObs$.pipe(map(arg => { return 'something'}));
You've done this:
someObs$.pipe(map(arg => {
map(arg => { return 'something' }) // this line here does nothing
return arg;
}));
It doesn't make any sense to use map inside the function you've given to map

How to combine two observables to create new observable?

I have two services named 'PatientsService' and 'AppointmentService'. In third service 'AppointedPatientsService', I want to subscribe to AppointmentService to get all booked appointments with patientId and after that I want to repeatedly subscribe to PatientsService.getPatient(patientId) to get Patient's data with patientId. And then, I want to return new array named allAppointedPatients which holds all appointments with patient's data. I tried this...
getAppointments() {
let allAppointments: Appointment[] = [];
const allAppointedPatients: AppointedPatient[] = [];
return this.appointmentService.fetchAllAppointments().pipe(
take(1),
tap(appointments => {
allAppointments = appointments;
for (const appointment of allAppointments) {
this.patientsService.getPatient(appointment.patientId).pipe(
tap(patient => {
const newAppointment = new AppointedPatient(patient.firstName,
patient.lastName,
patient.address,
patient.casePaperNumber,
appointment.appointmentDateTime);
allAppointedPatients.push(newAppointment);
})
).subscribe();
}
return allAppointedPatients;
}),
pipe(tap((data) => {
return this.allAppointedPatients;
}))
);
}
This is not working and I know there must be better way to handle such scenario. Please help...
You are messing up the async code (observables) with sync code by trying to return the allAppointedPatients array synchronously.
Understand first how async code is working in Javascript and also why Observables (streams) are so useful.
Try the code below and make sure you understand. Of course, I was not able to test it so make your own changes if needed.
getAppointments(): Observable<AppointedPatient[]> {
return this.appointmentService.fetchAllAppointments()
.pipe(
switchMap(appointments => {
const pacientAppointments = [];
for (const appointment of allAppointments) {
// Extract the data aggregation outside or create custom operator
const pacientApp$ = this.patientsService.getPatient(appointment.patientId)
.pipe(
switchMap((pacient) => of(
new AppointedPatient(
patient.firstName,
patient.lastName,
patient.address,
patient.casePaperNumber,
appointment.appointmentDateTime
)
))
)
pacientAppoinments.push(pacientApp$);
}
return forkJoin(pacientAppointments);
});
}
You can use forkJoin:
forkJoin(
getSingleValueObservable(),
getDelayedValueObservable()
// getMultiValueObservable(), forkJoin on works for observables that complete
).pipe(
map(([first, second]) => {
// forkJoin returns an array of values, here we map those values to an object
return { first, second };
})
);

Chaining rxjs operators

I have a function called fetch which is implemented like this:
fetch(): Observable<Option> {
return this.clientNotebookService.getClientNotebook()
.pipe(
map(
clientNotebook => {
this.person = _.find(clientNotebook.persons, i => i.isn === this.isn);
return {
value: this.person.address['TownIsn'],
name: this.person.address['TownName']
};
}
)
);
}
where person is of type RelatedPerson and isn if of type string. The problem with this function is that it doesn't know anything about isn, that's to say, isn should be provided to it in some way.
This value can be extracted in the following way:
this.activatedRoute.params.subscribe(
params => this.isn = params['id']
)
And I would like to do this within the fetch method by combining rxjs operators.
You can chain them like below, just extract the 'id' and pass it to fetch
const isn=this.activatedRoute.params.pipe(pluck('id'))
isn.pipe(mergeMap(isn=>fetch(isn)))
Then add a param to your fetch, then it is available inside the function
fetch(isn){ .....
You can use combineLatest that emits an array of the latest value from both streams every time either emit a value
combineLatest(
this.activatedRoute.params.pipe(map(params => params['id'])),
this.clientNotebookService.getClientNotebook()
).pipe(map(([isn, clientNotebook]) => {
this.person = clientNotebook.persons.find(p => p.isn === isn); // No need for _
return {
value: this.person.address['TownIsn'],
name: this.person.address['TownName']
};
}));

Angular RxJs - fill array in the second stream (nested stream)

I have in my Angular application a list of a specific model. Each item of this list has a propery xyzList. This xyzList property should be filled from a request which depends on the id of the first request. Here an example:
Model:
{
id: number;
name: string;
xyzList: any[]
}
Now I have two requests:
Request 1: Fills the model, but not the xyzList
this.myVar$ = this.myService.getElements<Model[]>();
Request 2: Should fill xyzList
this.myService.getXyzElements<XyzModel[]>(idOfTheModelOfTheFirstRequest);
At first I thought something like that:
this.myService.getElements<Model[]>()
.pipe(
mergeMap(items => items)
mergeMap(item => {
return this.myService.getXyzElements<XyzModel[]>(item.id)
.pipe(
map(xyzList => {
item.xyzList = xyzList
return item;
})
)
})
)
This does not work as I have flattened my list and I need an Observable<Array[]>, but I think is more or less clear what I want to achieve. I assume I can achieve this with forkJoin for instance, but I don't know how.Or is there a way to convert the flattened list back to list?
You need to use toArray(), because mergeMap/flatMap is used to flatten the array of data stream. It will return an Array instead of data steam.
this.myService.getElements<Model[]>()
.pipe(
mergeMap(items => items)
mergeMap(item => {
return this.myService.getXyzElements<XyzModel[]>(item.id)
.pipe(
map(xyzList => {
item.xyzList = xyzList
return item;
})
)
}),
toArray()
)
Well, I understand that you have a service function that return a list of "items". EachItem you need call another service function that return the data "xyzList".
So, each "item" must be a call, then, you create an array of "calls"
myService.getElements().pipe(
switchMap(result=>{
//We can not return the "list"
let calls:Observable[]=[]
//Before, the first calls was the own result
calls.push(of(result));
//Each result push the calls
result.forEach(e=>{
calls.push(myService.getXyzElements(e.id));
})
return forkJoin(calls).pipe(map(result=>{
//in result[0] we have the list of items
//In result[1] we have the Xyz of items[0]
//In result[2] we have the Xyz of items[1]
int i:number=0;
result[0].map(x=>{
i++;
return {
...x,
xyzList:result[i]
}
})
return result[0];
}))
})
).subscribe(res=>console.log(res));

How to return paginated data from the store OR new data from an api service using ngrx-effects in an angular2 app?

My question is a continuation of this excellent question and answer concerning the shape of paginated data in a redux store. I am using ngrx/store in an angular 2 app.
{
entities: {
users: {
1: { id: 1, name: 'Dan' },
42: { id: 42, name: 'Mary' }
}
},
visibleUsers: {
ids: [1, 42],
isFetching: false,
offset: 0
}
}
Based on the above shape I believe if the offset (or page, sort, etc.) from an incoming request payload changed then the visible users would change as well as the user entities by calling the DB. I have some actions and reducer functions to handle this and it works as expected. If the offset remains the same and the user is returning to the page the way they left it then the user entities should be returned by the store not the DB.
Where I am struggling is where to put that logic and which rxjs operators to use (still learning this).
I think the correct place is an effect. Here is what I have now in my angular2 app (I am injecting Actions, Store, and my UserService) that pulls new data every time the page is loaded.
#Effect loadUsers$ = this.actions$
.ofType('LOAD_USERS')
.switchMap(() => this.userService.query()
.map((results) => {
return new LoadUsersSuccessAction(results);
}))
.catch(() => Observable.of(new LoadUsersFailureAction()));
My best idea is something like this:
#Effect loadUsers$ = this.actions$
.ofType('LOAD_USERS')
.withLatestFrom(this.store.select(state => state.visibleUsers.offset))
.switchMap(([action, state]) => {
//something that looks like this??
//this syntax is wrong and I can't figure out how to access the action payload
state.offset === payload.offset
? this.store.select(state => state.entities.users)
: this.userService.query()
}
.map((results) => {
return new LoadUsersSuccessAction(results);
}))
.catch(() => Observable.of(new LoadUsersFailureAction()));
Not sure how to make this work. Thanks ahead.
I don't like answering my own questions but it took me quite a while to find the answer to this. I won't accept this answer as I am not sure this is the best way to go about things (still learning the ins/outs). It does however, work perfectly.
I found the correct syntax in a github gist. This code does not match my example but it clearly demonstrates "2 options for conditional ngrx effects" by either returning a store or api observable using some sort of condition.
Hope this helps someone.
Here it is:
#Effect()
selectAndLoadStore$: Observable<Action> = this.actions$
.ofType(storeActions.SELECT_AND_LOAD_STORE)
.withLatestFrom(this.store.select(ngrx.storeState))
.map(([action, storeState]) => [action.payload, storeState])
.switchMap(([storeName, storeState]) => {
const existsInStore = Boolean(storeState.urlNameMap[storeName]);
return Observable.if(
() => existsInStore,
Observable.of(new storeActions.SetSelectedStore(storeName)),
this.storeService.getByUrlName(storeName)
.map(store => new storeActions.LoadSelectedStoreSuccess(store))
);
});
#Effect()
selectAndLoadStore$: Observable<Action> = this.actions$
.ofType(storeActions.SELECT_AND_LOAD_STORE)
.withLatestFrom(this.store.select(ngrx.storeState))
.map(([action, storeState]) => [action.payload, storeState])
.switchMap(([storeName, storeState]) => {
const existsInStore = Boolean(storeState.urlNameMap[storeName]);
let obs;
if (existsInStore) {
obs = Observable.of(new storeActions.SetSelectedStore(storeName));
} else {
obs = this.storeService.getByUrlName(storeName)
.map(store => new storeActions.LoadSelectedStoreSuccess(store));
}
return obs;
});

Categories

Resources