implement infinite scroll with rxjs - javascript

I'm having trouble when implement infinite scroll with rxjs
I have tried following snippet
var lastid = null
fromEvent(window, 'scroll')
.pipe(
filter(() => isAtPageBottom()),
exhaustMap(() => from(this.getList(lastid))),
takeWhile(list => list.length !== 0),
scan((cur, list) => [...cur, ...list], [])
)
.subscribe(list => {
this.setState({list: list})
})
async function getList (lastid) {
const list = await request('/api/list?lastid=' + lastid)
listid = list[list.length-1].id
return list
}
How to pass the lastid to each request without the global variable 'lastid'?
Thanks

haven't test the code but you can try the below code snippet. The technique here is to start off with a state that contains your list and lastId then use expand to keep the stream going and listen for scroll
of({ list: [], lastId: null }).pipe(
expand((obj) => {
return fromEvent(window, 'scroll')
.pipe(
filter(() => isAtPageBottom()),
exhaustMap(() => from(this.getList(obj.lastid))),
map(res => ({ list: obj.list.concat(res), lastId: res[res.length - 1].id }))
)
}),
takeWhile(({ list }) => list.length !== 0))

Related

Why first element of array is empty?

I have a function which returns an Observable and once its fires I'm looping the subscribed array
this.productService.getProductBySeo(proudctName).pipe(
catchError(e => of(null)),
this.unsubsribeOnDestroy
).subscribe(res => {
if (res) {
this.categoriesService.currentCategory.subscribe(menus => {
const activeElem = res;
const allItems = menus;
allItems.map(item => {
console.log(item)
console.log(item.id) // can see id of each elem
console.log(item.children) // first element of item.children is empty
....
UPD 1
I have changed to switchMap as it was advised in comments , anyway the arrays is empty
this.productService.getProductBySeo(proudctName).pipe(
catchError(e => of(null)),
switchMap(res => {
this.categoriesService.currentCategory.subscribe(menus => {
if (menus) {
const activeElem = res;
const allItems = menus;
allItems.map(item => {
console.log(item)
// console.log(item.id) // can see id ,
console.log(item.children) // still empty :-(
item.children.map(item => {
// console.log(item);
return item
})
const fountObj = item.children.find(el => {
// console.log('element ', el)
return el === activeElem;
});
if (fountObj) {
this.totalStyle = item.seo_url; // to identify total style of the component
}
})
}
// console.log(menus)
})
return of(res)
}),
this.unsubsribeOnDestroy
).subscribe(res => {
if (res) {
this.product = res;
Regarding what do I expect to see is when subscription fires in this.categoriesService.currentCategory.subscribe, I want to see item.children in console. As I see only empty array when log item.children , but when I console just item, I can see that item.children has 2 items in own Array
Not a great solution, But I suspect that it might have been changed by reference on somewhere else. Try deep cloning and looping.
i.e.
const allItems = JSON.parse(JSON.stringify(menus));
allItems.forEach(item => {
console.log(item)
console.log(item.children)
Also, forEach looks much better than a map in such circumstances.

Emmit old value if SwitchMap has error in Rxjs

I am trying to get a member value from other web api in rxjs.
I did this in a pipe method with switchMap. But if there is a problem with getting the member value then I want to skip old model with values to next method.
So I dont want to return null after switchMap worked.
Here is my code:
(this.repository.getData(`employeecards/list/${this.currentUser.companyId}`) as Observable<EmployeeCard[]>)
.pipe(
flatMap(emp => emp),
tap(emp => emp),
switchMap((empCard: EmployeeCard) => this.repository.getData(`cards/${empCard.cardId}`),(empCard, card) => ({ empCard, card }) ),
//second subscribe
switchMap((emp: { empCard: EmployeeCard, card: LogisticCard }) => this.parraApiService.getBalanceByBarcode(emp.card.barcode),
(emp: { empCard: EmployeeCard, card: LogisticCard }, apiRes: ParraApiResult) => {
if (apiRes.response.isSuccess) {
//I use this subscribe only set to balance
emp.empCard.balance = apiRes.response.data['balance'];
}
return emp.empCard
}),
catchError((e) => {
return of([]); //I want to retun emp value
}),
reduce((acc, value) => acc.concat(value), [])
)
How can I solve this problem?
Thanks
I think you can achieve that by creating a closure:
(this.repository.getData(`employeecards/list/${this.currentUser.companyId}`) as Observable<EmployeeCard[]>)
.pipe(
flatMap(emp => emp),
tap(emp => emp),
switchMap((empCard: EmployeeCard) => this.repository.getData(`cards/${empCard.cardId}`), (empCard, card) => ({ empCard, card })),
switchMap(
(emp: { empCard: EmployeeCard, card: LogisticCard }) => this.parraApiService.getBalanceByBarcode(emp.card.barcode)
.pipe(
map((emp: { empCard: EmployeeCard, card: LogisticCard }, apiRes: ParraApiResult) => {
if (apiRes.response.isSuccess) {
emp.empCard.balance = apiRes.response.data['balance'];
}
return emp.empCard
}),
catchError((e) => {
// `emp` available because of closure
return of(emp);
}),
)
),
reduce((acc, value) => acc.concat(value), [])
)
Also notice that I gave up on switchMap's custom resultSelector, as it can easily be replaced with a map operator.

Recursion and Observable RxJs

I am performing pagination inside and Observable stream.
The pagination is implemented with a cursor and a total count using recursion.
I am able to emit the every page using the following code observer.next(searches);, by the way I would like to use just observable and no promises but I cannot express recursion using RxJs operators.
Any suggestions?
const search = id =>
new Observable(observer => { recursePages(id, observer) })
const recursePages = (id, observer, processed, searchAfter) => {
httpService.post(
"http://service.com/search",
{
size: 50,
...searchAfter ? { search_after: searchAfter } : null,
id,
})
.toPromise() // httpService.post returns an Observable<AxiosResponse>
.then(res => {
const body = res.data;
const searches = body.data.hits.map(search => ({ data: search.data, cursor: search.id }));
observer.next(searches);
const totalProcessed = processed + searches.length;
if (totalProcessed < body.data.total) {
return recursePages(id, observer, totalProcessed, searches[searches.length - 1].cursor);
}
observer.complete();
})
}
// General Observer
incomingMessages.pipe(
flatMap(msg => search(JSON.parse(msg.content.toString()))),
concatAll(),
).subscribe(console.log),
these methods will recursively gather all the pages and emit them in an array. the pages can then be streamed with from as shown:
// break this out to clean up functions
const performSearch = (id, searchAfter?) => {
return httpService.post(
"http://service.com/search",
{
size: 50,
...searchAfter ? { search_after: searchAfter } : null,
id,
});
}
// main recursion
const _search = (id, processed, searchAfter?) => {
return performSearch(id, searchAfter).pipe( // get page
switchMap(res => {
const body = res.data;
const searches = body.data.hits.map(search => ({ data: search.data, cursor: search.id }));
const totalProcessed = processed + searches.length;
if (totalProcessed < body.total) {
// if not done, recurse and get next page
return _search(id, totalProcessed, searches[searches.length - 1].cursor).pipe(
// attach recursed pages
map(nextPages => [searches].concat(nextPages)
);
}
// if we're done just return the page
return of([searches]);
})
)
}
// entry point
// switch into from to emit pages one by one
const search = id => _search(id, 0).pipe(switchMap(pages => from(pages))
if what you really need is all of the pages to emit one by one before they're all fetched, for instance so you can show page 1 as soon as it's available rather than wait on page 2+, then that can be done with some tweaking. let me know.
EDIT: this method will emit one by one
const _search = (id, processed, searchAfter?) => {
return performSearch(id, searchAfter).pipe( // get page
switchMap(res => {
const body = res.data;
const searches = body.data.hits.map(search => ({ data: search.data, cursor: search.id }));
const totalProcessed = processed + searches.length;
if (totalProcessed < body.total) {
// if not done, concat current page with recursive call for next page
return concat(
of(searches),
_search(id, totalProcessed, searches[searches.length - 1].cursor)
);
}
// if we're done just return the page
return of(searches);
})
)
}
const search = id => _search(id, 0)
you end up with an observable structure like:
concat(
post$(page1),
concat(
post$(page2),
concat(
post$(page3),
post$(page4)
)
)
)
and since nested concat() operations reduce to a flattened structure, this structure would reduce to:
concat(post$(page1), post$(page2), post$(page3), post$(page4))
which is what you're after and the requests run sequentially.
it also seems like expand might do the trick as per #NickL 's comment, soemthing like:
search = (id) => {
let totalProcessed = 0;
return performSearch(id).pipe(
expand(res => {
const body = res.data;
const searches = body.data.hits.map(search => ({ data: search.data, cursor: search.id }));
totalProcessed += searches.length;
if (totalProcessed < body.data.total) {
// not done, keep expanding
return performSearch(id, searches[searches.length - 1].cursor);
}
return EMPTY; // break with EMPTY
})
)
}
though I've never used expand before and this is based off some very limited testing of it, but I am pretty certain this works.
both of these methods could use the reduce (or scan) operator to gather results if you ever wanted:
search(id).pipe(reduce((all, page) => all.concat(page), []))
This is my used solution combining the expand and reduce operator
searchUsers(cursor?: string) {
return from(
this.slackService.app.client.users.list({
token: this.configService.get('SLACK_BOT_TOKEN'),
limit: 1,
...(cursor && { cursor }),
}),
);
}
Usage
.......
this.searchUsers()
.pipe(
expand((res) => {
if (!!res.response_metadata.next_cursor) {
return this.searchUsers(res.response_metadata.next_cursor);
}
return EMPTY;
}),
reduce((acc, val) => {
return [...acc, ...val.members];
}, []),
)
.subscribe((users) => {
console.log(JSON.stringify(users));
});
....

Is there a way to use a observable returning function for each element of another observable array?

I get an Observable<Group[]> from my Firebase collection.
In this Group class is an id which I wanna use to retrieve another dataset array from Firebase, which would be messages for each unique group Observable<Message[]>.(each group has its own chat: Message[])
And it want to return an observable which hold an array of a new Type:
return { ...group, messages: Message[] } as GroupWithMessages
the final goal should be Observable<GroupWithMessages[]>
getGroupWithChat(): Observable<GroupWithMessages[]> {
const groupColl = this.getGroups(); // Observable<Group[]>
const messages = groupColl.pipe(
map(groups => {
return groups.map(meet => {
const messages = this.getMessagesFor(group.uid);
return { messages:messages, ...group} as GroupWithMessages
});
})
);
return messages;
}
}
and here the Message function
getMessagesFor(id: string): Observable<Message[]> {
return this.afs.collection<Message>(`meets/${id} /messages`).valueChanges();
}
sadly that doesnt work because when i create the new Obj I cannot bind messages:messages because messages ist vom typ Observable<Message[]>
I hope that cleares things
UPDATE:
my main problem now comes down to this:
getGroupsWithMessages() {
this.getJoinedGroups()
.pipe(
mergeMap(groups =>
from(groups).pipe(
mergeMap(group => {
return this.getMessagesFor(group.uid).pipe(
map(messages => {
return { ...group, messages } as GroupIdMess;
})
);
}),
tap(x => console.log('reaching here: ', x)),
toArray(),
tap(x => console.log('not reaching here = completed: ', x))
)
),
tap(x => console.log('not reaching here: ', x))
)
.subscribe(x => console.log('not reaching here: ', x));
}
when i call that function my console.log is as follows:
Not sure if I follow what you're doing here but the logic look like you'd want:
getGroupWithChat() {
return this.getGroups.pipe(map(groups=> {
return groups.map(group => this.getMessagesFor(group.uid));
})).subscribe(); // trigger "hot" observable
}
Let me know if I can help further after you clarify.
UPDATE:
So it looks like you need to get the UID of the group before making the call to get the GroupMessages[]?
get Group: Observable
call getMessagesFor(Group.uid)
this example gets groups result$ then
concatMap uses groups result$ to make the messages query
this.getGroups().pipe(
concatMap((group: Group) => this.getMessagesFor(group.uid))
).subscribe((messages: GroupWithMessages[]) => {
console.log(messages);
});
You may still want to map them together but it seems like you know how to do that. concatMap waits for the first to finish, then makes the second call which you need.
Is this closer?
Use forkJoin to wait for messages to be received for all groups. Then map the result of forkJoin to an array of GroupWithMessages like this -
getGroupWithChat(): Observable<GroupWithMessages[]> {
return this.getGroups()
.pipe(
switchMap(groups => {
const messagesForAllGroups$ = groups.map(group => this.getMessagesFor(group.uid));
return forkJoin(messagesForAllGroups$)
.pipe(
map(joined => {
//joined has response like -
//[messagesArrayForGroup0, messagesArrayForGroup1, messagesArrayForGroup2....];
const messagesByGroup = Array<GroupWithMessages>();
groups.forEach((group, index) => {
//assuming that GroupWithMessages has group and messages properties.
const gm = new GroupWithMessages();
gm.group = group;
gm.messages = joined[index];
messagesByGroup.push(gm);
});
return messagesByGroup;
})
)
})
)
}
I usually do that by splitting Observable<any[]> to Observable<any> and then mergeMap the results to inner Observable.
Something like this should work:
getMessagesFor(id: string): Observable<number> {
return of(1);
}
getGroups(): Observable<string[]> {
return of(["1", "2"]);
}
getGroupWithChat() {
this.getGroups().pipe(
mergeMap(groups => from(groups)), // Split the stream into individual group elements instead of an array
mergeMap(group => {
return this.getMessagesFor(group).pipe(
map(messages => {
return Object.assign(group, messages);
})
);
})
);
}
Edit:
Consider BehaviorSubject. It doesn't complete at all:
const behSub: BehaviorSubject<number[]> = new BehaviorSubject([1, 2, 3]);
setTimeout(() => {
behSub.next([4, 5, 6]);
}, 5000);
behSub
.pipe(
mergeMap(arr =>
from(arr).pipe(
tap(), // Do something with individual items, like mergeMap to messages
toArray() // Go back to array
)
)
)
.subscribe(console.log, null, () => {
console.log('Complete');
});

Filtering Observable with Rxjs

I'm trying to get a list of active jobs for the current user:
jobListRef$: Observable<Job[]>;
...
this.afAuth.authState.take(1).subscribe(data => {
if (data && data.uid) {
this.jobListRef$ = this.database.list<Job>('job-list', query => {
return query.orderByChild("state").equalTo("active");
})
.snapshotChanges().map(jobs => {
return jobs.map(job => {
const $key = job.payload.key;
const data = { $key, ...job.payload.val() };
return data as Job;
});
})
.filter(val =>
val.map(job => {
return (job.employer == data.uid || job.employee == data.uid);
})
);
}
});
The problem begins only at the filtering. According to the official documentation the right part of its argument should return boolean, so like in my case. But still it returns me whole entries without filtering them.
You're returning the result of the Array.map call, see its return value: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
It looks like you maybe want to use map instead of filter:
.snapshotChanges().map(...)
.map(val => val.map(job => {
return (job.employer == data.uid || job.employee == data.uid);
}));
I believe you want to reverse the map and filter operators.
.map((jobs: Job[]) =>
jobs.filter((job: Job) => job.employer === data.uid || job.employee === data.uid )
);
(map to transform one array into another, filter to reduce the array).
Or you can chain filter on to the map that performs the type-conversion,
.map(jobs => {
return jobs.map(job => {
const $key = job.payload.key;
const data = { $key, ...job.payload.val() };
return data as Job;
})
.filter(job => job.employer === data.uid || job.employee === data.uid )
})
Not sure to understand the whole problem, but it might be something like:
this.jobListRef$ = this.afAuth.authState
.filter(data => !!data && !!data.uid)
.take(1)
.switchMap(data =>
this.database.list<Job>('job-list', query => query.orderByChild("state").equalTo("active"))
.snapshotChanges()
.map(jobs =>
jobs.map(job => {
const $key = job.payload.key;
const data = { $key, ...job.payload.val() };
return data as Job;
})
)
.map((jobs: Job[]) =>
jobs.filter(job => (job.employer == data.uid || job.employee == data.uid))
)
);

Categories