I am building a Chat application, and when a new message gets added to a list, I need to update the chat item that contains the message list.
I am using AngularFire2, and have an Observable. This Observable works perfectly to dynamically maintain a list as expected.
I do however, have a conundrum. When the list changes I need to perform an update which in turn causes the list to change, and so on resulting in an infinite loop.
code:
The following is the Observable
findMessages(chatItem: any): Observable<any[]> {
return this.af.database.list('/message/', {
query: {
orderByChild: 'negativtimestamp'
}
}).map(items => {
const filtered = items.filter(
item => ((item.memberId1 === chatItem.memberId1 && item.memberId2 === chatItem.memberId2)
|| (item.memberId1 === chatItem.memberId2 && item.memberId2 === chatItem.memberId1))
);
return filtered;
});
}
I populate a list with this Observable
findAllMessages(chatItem: any): Promise<any> {
return this.firebaseDataService.findMessages(chatItem).forEach(firebaseItems => {
chatItem.lastMsg_text = 'updated value';
this.firebaseDataService.updateChat(chatItem);
});
}
As you can see, I update the chatItem
updateChat(chatItem: any): firebase.Promise<void> {
return this.findChat(chatItem).forEach((chatItems: any[]) => {
if (chatItems.length > 0) {
for (let i: number = 0; i < chatItems.length; i++) {
return this.af.database.object('/chat/' + chatItems[i].$key).update({
timestamp: chatItem.timestamp,
negativtimestamp: chatItem.negativtimestamp,
lastMsg_text: chatItem.lastMsg_text,
lastMsg_read1: chatItem.lastMsg_read1,
lastMsg_read2: chatItem.lastMsg_read2
});
}
} else {
this.createChatFromItem(chatItem);
}
});
}
Question
My confusion here, is that I am updating the chats and not the messages, so why is the messages Observable being triggered? Is there a way to achieve this avoiding the infinite loop?
Any help appreciated.
Seems like you could handle all of this in a much simpler way... The way you mixed Promises & Observables feels a bit convoluted.
Have you looked into Subject?
To put it very simply, you could have a Subject "publishing" any changes in your chat application by calling
yourSubject.next(newChatMessage)
From then on, subscribing to that Subject from any parts of your application, you'd be able to make any updates necessary.
Then if you want to dig deeper into the subject of state management, you could have a look at the following:
RxJS powered state management for Angular applications, inspired by Redux
Related
The below is my .ts file for the Alarm Component and over HTML I am using a simple *ngFor over criticalObject.siteList to display the records
This is not the original code I have simplified this but the problem I am facing is that on rigorous click on the refresh button(fires HTTP request), the list is adding duplicate siteNames and that should not happen. I have heard of debounce time, shareReplay, and trying applying here, which even doesn't make sense here.
NOTE: I have to fire the HTTP request on every refresh button click.
Keenly Waiting for Help.
criticalObject.siteList = [];
siteList = ["c404", "c432"];
onRefresh() {
this.criticalObject.siteList = [];
this.siteList.forEach(elem => {
getAlarmStatus(elem);
})
}
getAlarmStatus(item) {
critical_list = [];
alarmService.getAlarmStatusBySite(item.siteName).subcribe(data => {
if(data) {
// do some calculations
if(this.criticalObject.siteList.length === 0) {
this.criticalObject.siteList.push({
siteName = item.siteName;
})
}
this.criticalObject.siteList.forEach((elem, idx) => {
if(elem.siteName === item.siteName) {
return;
} else if(idx === this.criticalObject.siteList.length - 1) {
this.criticalObject.siteList.push({
siteName = item.siteName;
})
}
})
}
}
})
I did a silly mistake, I am new to JavaScript, I found out you cannot return from a forEach loop and that's why I was getting duplicated records, return statement in forEach acts like a continue in JavaScript.
Am fairly new to RxJs, and trying to wrap my head around what the proper pattern is to simply create an Observable array of Observables.
I want to retrieve a list of User's Posts. The Posts themselves should be Observables, and I want to keep them all in an Observable array, so that when the array changes the calling code should be notified and update anything subscribed to the post "list". This is simple enough, but I also would like each of the Posts to be Observables, so if I retrieve a specific posts[i] from it, I should also be able to subscribe to these individual objects.
What is the proper way to do this?
Am using Angular 9, I have:
public getPosts(): Observable<Array<Post>> {
return new Promise((resolve, reject) = {
let posts: Observable<Array<Post>> = new Observable<Array<Post>>();
this.get<Array<Post>>('posts').subscribe(r => {
posts = from(r);
return resolve(posts);
});
});
}
This gives me an Observable<Array<Post>>, but how should I create an Observable<Array<Observable<Post>>>?
Is this an anti-pattern?
It all comes to convenience, if your server serves you differential data of what changed in post, then go ahead and create Observable<Observable<Post>[]>.
In your post, however, there are multiple problems. You cannot mix Observables with Promises. The method getPosts will return only the first post you get from API.
This is the solution ask for, but I am not sure, it is what you actually wanted...
public getPosts(): Observable<Array<Observable<Post>>> {
return this.get('posts').pipe(
switchMap(posts => combineLatest(
posts.map(post => this.get('post', post.id))
)),
);
}
it's unclear what you're trying to accomplish here, but you might want something more like this:
#Injectable({providedIn:'root'})
export class PostService {
// private replay subject will cache one value
private postSource = new ReplaySubject<Post[]>(1)
// public list of posts observable
posts$ = this.postSource.asObservable();
// function to select item by id out of list
post$ = (id) => this.posts$.pipe(map(posts => posts.find(p => p.id === id)))
getPosts() {
// function to get remote posts
return this.get<Post[]>('posts');
}
loadPosts() {
// function to load posts and set the subject value
this.getPosts().subscribe(posts => this.postSource.next(posts));
}
}
you'll have to define that get function and call loadPosts everytime you want to update the list.
Given informations:
!If any of this statements is wrong, please tell me and I will update the answer!
get function that returns an observable with one array that filled with posts
the get observable emits always when the posts are changing
the value inside the observable (Array>) is no observable and does not change over time
this.get<Array<Post>>('posts')
Possible functions
() => getPostById$
// This function returns you an observable with the post related to your id.
// If the id is not found the observable will not emit
// If the id is found the observable will only emit if the interface values have been changed
function getPostById$(id: string): Observable<Post> {
// Returns you either the post or undefined if not found
const findId = (id: string) => (posts: Array<Post>): Post | undefined =>
posts.find(post => post.id === id);
// Allows you only to emit, if id has been found
const existingPost = (post: Post | undefined): boolean => post != null;
// Allows you only to emit if your id has been changed
const postComparator = (prevPost: Post, currPost: Post): boolean =>
prevPost.value === currPost.value && prevPost.name === currPost.name;
return this.get('posts').pipe(
map(findId(id)),
filter(existingPost),
distinctUntilChanged(postComparator)
);
}
() => getPosts$
function getPosts$(): Observable<Array<Post>> {
return this.get('posts');
}
() => getStatePosts$
// This function allows to manage your own state
// 1. posts$: overwrites all posts
// 2. clear$: empties your posts$ observable
// 3. add$: adds one observable to the end of your posts
function statePosts$(posts$: Observable<Array<Posts>>, clear$: Observable<void>, add$: Observable<Post>): Observable<Array<Post>> {
const updatePosts = (newPosts: Array<Posts>) => (oldPosts: Array<Posts>) => newPosts;
const clearPosts = () => (oldPosts: Array<Posts>) => [];
const addPost = (post: Post) => (oldPosts: Array<Posts>) => [...oldPosts, post];
return merge(
// You can add as much update functions as you need/want (eg: deleteId, addPostAtStart, sortPosts, ...)
posts$.pipe(map(updatePosts)),
clear$.pipe(map(clearPosts)),
add$.pipe(map(addPost))
).pipe(
// The fn in you scan is the (oldPosts: Array<Posts>) function from one of your three update functions (updatePosts, clearPosts and addPosts).
// Whenever one of those three observables emits it first calls the left side of the function inside the map (post: Post) and returns a new function
// When this function reaches the scan it gets the oldPosts and is able to update it
scan((oldPosts, fn) => fn(oldPosts), [])
)
}
// Usage
private posts$: Observable<Array<Post>> = this.get('posts');
private clear$: Subject<void> = new Subject();
private add$: Subject<Post> = new Subject();
public statePosts$ = getStatePosts(posts$, clear$, add$);
Hint: Try to read the functions from the return statement first. And then check what is happening in the mapping/filtering or other operations. Hopefully I did not confuse you too much. If you have questions, feel free to ask.
I'm trying to optimize the speed (happens to make my code faster). How can I optimize below piece of code so that load time get optimize further. And if possible, please suggest what I should usually keep in mind.
As of now Finish time = 2.45sec.
TS
export class AppComponent implements OnInit {
searchKeywords: string;
CoffeeItemList: any = [];
type: string;
search: string;
selectedType = '';
showLoader: boolean;
empty = false;
data: any = [];
// tslint:disable-next-line:max-line-length
constructor(private getDataListingService: DataListingService) {}
ngOnInit(): void {
this.getGlobalSearchList('');
this.getAllData();
this.getSmartSearchValues('');
if (this.CoffeeItemList.length === 0) {
this.empty = true;
}
}
getAllData() {
this.showLoader = true;
this.getDataListingService.getAllDataLists().subscribe(value => {
this.CoffeeItemList = value.data;
this.showLoader = false;
});
}
getGlobalSearchList(type: string) {
this.selectedType = type;
this.CoffeeItemList = [];
this.getDataListingService.getAllDataLists().subscribe(value => {
this.data = value.data;
console.log(this.data);
// tslint:disable-next-line:prefer-for-of
for (let i = 0; i < this.data.length; i++) {
if (this.data[i].type === type) {
this.CoffeeItemList.push(this.data[i]);
}
}
});
}
getSmartSearchValues(search: string) {
if (search === '' ) {
this.getAllData();
return false;
}
if (search.length >= 3) {
this.getDataListingService.searchList(search).subscribe(value => {
this.data = value.data;
this.CoffeeItemList = value.data;
// check selected type either coffee, mobile or ALL.
if (this.selectedType && this.selectedType !== '' ) {
this.CoffeeItemList = [];
// tslint:disable-next-line:prefer-for-of
for (let i = 0; i < this.data.length; i++) {
if (this.data[i].type === this.selectedType) {
this.CoffeeItemList.push(this.data[i]);
}
}
}
});
}
}
}
I can see that you call your service getDataListingService three times in ngOnInit() and every time you make a request and, I suppose, you collect data and then you work on data.
I'd like to see your HTML file as well. Maybe you don't need to make that much requests on init.
First of all you call getAllDataLists twice why?
Can't one of these subscribe be made redundant?
Second try to filter out more data cause atleast 1 second for a call is alot of data getAllDataLists should be filter before you load all the data into your application.
Either it's to much data so you should check your network tab. Or the html is to complicated and load way to much data and that's why your aplication is getting so slow.
Also I see you're making the same mistakes with subscription?
You know it will trigger everytime data get changed? So with your current setup and if you subscribe on every keyup you will create 100 of subsciptions that never finish and keep poling for changes.
https://stackoverflow.com/a/60389896/7672014
Based on the code you have shared:
You need to add some sort of pagination to your 'getAllDataLists' & 'searchList' methods i.e. you always get results in chunks instead of getting larger data results to render on the UI.
The following methods when called are performing API calls that can all be combined into 1 single API call to reduce the memory load and reduce wait time for user to see results/data being rendered on the page.
this.getGlobalSearchList('');
this.getAllData();
this.getSmartSearchValues('');
Optimize your API call e.g. send the 'selectedType' in the API itself to already get filtered results from the backend instead of adding a check of if (this.selectedType && this.selectedType !== '' )
In a parent component I have a stream of Tour[] tours_filtered: Observable<Tour[]> which I assign in the subscribe function of an http request
this.api.getTours().subscribe(
result => {
this.tours_filtered = of(result.tours);
}
)
in the view I display the stream using the async pipe
<app-tour-box [tour]="tour" *ngFor="let tour of tours_filtered | async"></app-tour-box>
Up to here all works as expected. In a child component I have an input text which emits the value inserted by the user to filtering the array of Tour by title.
In the parent component I listen for the emitted values in a function, I switch to new stream of Tour[] filtered by that value using switchMap
onSearchTitle(term: string) {
this.tours_filtered.pipe(
switchMap(
(tours) => of( tours.filter((tour) => tour.name.toLowerCase().includes(term)) )
)
)
}
I thought that the async pipe was constantly listening to reflect the changes to the array to which it was applied and so I thought I didn't have to subscribe in the function above, but nothing change in the view when I type in the input to filtering the results.
The results are updating correctly if I assign the new stream to the original array in the subscribe function
onSearchTitle(term: string) {
this.tours_filtered.pipe(
switchMap((tours) => of(tours.filter((tour) => tour.name.toLowerCase().includes(term))))
).subscribe( val => { this.tours_filtered = of(val); })
}
Is this procedure correct? Could I avoid to subscribe because I already use the async pipe? There is a better way to reach my goal?
EDITED:
Maybe I found a solution, I have to reassing a new stream to the variable just like this
onSearchTitle(term: string) {
this.tours_filtered = of(this.city.tours).pipe(
switchMap((tours) => of(tours.filter((tour) => tour.name.toLowerCase().includes(term))))
);
}
and I don't need to subscribe again, the results in the view change according to the search term typed by the user. Is this the correct way?
I think in your situation the solution should work as follows:
onSearchTitle(term: string) {
this._searchTerm = term;
this.tours_filtered = of(
this.city.tours.filter((tour) => tour.name.toLowerCase().includes(term))
)
}
Because in your example you don't change the observable which is used in ngFor. Thus it's not working.
However, I don't see the reason of using observables here unless this is the first step and you're going to fetch this data from server in future
UPDATE
The best solution for you would be to consider your input as an observable and watch for the changes:
// your.component.ts
export class AppComponent {
searchTerm$ = new BehaviorSubject<string>('');
results = this.search(this.searchTerm$);
search(terms: Observable<string>) {
return terms
.pipe(
debounceTime(400),
distinctUntilChanged(),
switchMap(term => {
return of(this.city.tours.filter((tour) => tour.name.toLowerCase().includes(term)))
}
)
)
}
}
// your.template.html
...
<input type="" (input)="searchTerm$.next($event.target.value)">
...
Additionally it would be great to add debounceTime and distinctUntilChanged for better user experience and less search requests.
See full example for the details. Also please, refer to this article for more detailed explanations
I am developing a table component and the data for the table component is to be populated on basis of three dropdown values. I need to pass in all three values to the API to get the desired response. I can achieve it using nested subscribes, which is a very bad way. but any change calls the API multiple times. How can I fix it? Most examples I found are for getting only the final subscribe value but in my case, I need all three. Any advice to achieve using tap and flatMap?
Please advice.
this._data.currentGroup.subscribe(bg => {
this.bg = bg;
this._data.currentUnit.subscribe(bu => {
this.bu = bu;
this._data.currentFunction.subscribe(jf => {
this.jf = jf;
this.apiService.getFunctionalData(this.bg, this.bu, this.jf)
.subscribe(
(data) => {
console.log(data)
}
);
});
});
});
This is what I did.
this._data.currentGroup.pipe(
tap(bg => this.bg = bg),
flatMap(bu => this._data.currentUnit.pipe(
tap(bu => this.bu = bu),
flatMap(jf => this._data.currentFunction.pipe(
tap(jf => this.jf = jf)
))
))
).subscribe();
This is a sample example of my dataService. I initialize my dataservice in the table component's constructor as _data.
changeGroup(bg: string) {
this.changeGroupData.next(bg);
}
private changeGroupData = new BehaviorSubject<string>('');
currentChangeGroup = this.changeGroupData.asObservable();
You can use combineLatest to combine the three Observables and then subscribe to all of them at once. It will emit a new value as soon as one of the three Observables changes.
combineLatest(this._data.currentGroup,
this._data.currentUnit,
this._data.currentFunction).subscribe(([bg, bu, jf]) => {
// do stuff
});
For an example, have a look at this stackblitz demo I created.