I'm currently using combineLatest() method to get all my friends and make it as a list. However, it produces an ordering issue when I try to remove an item from the beginning. (View does not render correctly) Therefore, I'd like to switch from combineLatest() to forkJoin() to get all the friends at once.
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/forkJoin';
friends$: Observable<User[]>;
getMyFriendList() {
this.friends$ = this.userService.getMyFriendsId().switchMap(friendKeys => {
return Observable.forkJoin(friendKeys.map(user => this.userService.getFriends(user.key)));
});
}
But nothing happens when I call forkJoin(). What am I doing wrong here?
getMyFriendsId() {
let friendRef = this.getFriendRef(this.currentUserId);
this.friends$ = friendRef.snapshotChanges().map(actions => {
return actions.map(a => ({ key: a.payload.doc.id, ...a.payload.doc.data()}));
});
return this.friends$;
}
getFriends(uid: string) {
return this.getUsersRef(uid).valueChanges();
}
EDIT
getMyFriendsId() updates a new data when a logged in user remove another user in the friend list.
<ion-list>
<ion-list-header>Friends <span class="total-friends"></span></ion-list-header>
<ion-item-sliding *ngFor="let user of friends$ | async">
<ion-item *ngIf="user" (click)="viewUserProfile(user)">
<ion-avatar (click)="showOriginalAvatarImage()" item-start>
<img-loader [src]="user.thumbnailURL" [spinner]="true"></img-loader>
</ion-avatar>
<h2>{{user.displayName}}</h2>
<p>{{user.statusMessage}}</p>
</ion-item>
...
<ion-list>
Lifecycle event
ionViewWillEnter() {
// Runs when the page is about to enter and become the active page.;
this.getMyFriendList();
}
.forkJoin() combines all the observables, and will only emit the values if all observables are COMPLETED. The reason your code doesn't work is because a you are using valueChanges(), and it is a type of event that will never complete -- they are forever listening to the changes of the value!
If you really want to use .forkJoin (or to prove the point), add a take(1) into your valueChanges:
getFriends(uid: string) {
return this.getUsersRef(uid).valueChanges().take(1);
}
The above code will work because it forcefully completes the observable with take(), but obviously will defeat the purpose because your code will only work once. Conclusion is if you want to keep on observing for a change of a particular value, and combine it with another observable, yes, use combineLatest()
Related
I subsribed to observable of behaviorSubject and it's triggers too many times. It happens only when i am navigating on the same component route, as an example...folder-folder-folder and now ...delete file triggers x3 times.
Subscribe code:
this.headerService.selectedItems.subscribe( {
next:(value) =>
if (this.selectedRowsIds.size >= 1 && value === true) {
this.deleteDocs();
}... and here value comes x3 times
BehaviorSubject:
deleteButton = new BehaviorSubject<any>({});
selectedItems = this.deleteButton.asObservable();
deleteTrigger(trigger: boolean) : void {
this.deleteButton.next(trigger);
}
I tried to unsubcribe, to send false trigger everytime when i navigate, but nothing changes.
I mention that component DOES NOT DESTROY in this case, cause we open folder-folder-folder on the same component, with changing route params.
The Problem can due to many other factors
Due to any change in route in the process.
Due to Not emitting value at right time.
Behavior Subject fires the value as soon as its initialized new BehaviorSubject<any>({}) with an {} (and empty object it emits and this value it holds )
Insufficient working and dependable code to see the flow , if possible please provide insight.
My Solution:
deleteButton :BehaviorSubject<boolean>= new BehaviorSubject<boolean>(false);
deleteTrigger(trigger: boolean) : void {
this.deleteButton.next(trigger);
}
trigger: boolean (here we are only emitting boolean values because input is always boolean)
this.headerService.deleteButton.subscribe(response => {
if(response) {
this.selectedRowsIds.size >= 1 && value === true ?
this.deleteDocs() : '';
}
});
this would subscribe to the action when a value is emitted from deleteButton and the value is true then it would excute desired logic though tenary opeartor
You can use rxjs takeLast with behavior subject to fix the issue or Promise Resolve. takeLast, you could also use take, takeOnce all found in rxjs. Also you do not need to strong type your interfaces on behavior subjects. It's recommended for security concerns. because any is applied.
Also you should make your subscription async and await the answer from the subscription.
RXJS LINK
rxjs
import { BehaviorSubject, takeLast, tap } from 'rxjs';
private yourSubject = new BehaviorSubject<any>({});
//or your choice
private yourSubject = new BehaviorSubject({});
//or your choice
private yourSubject = new BehaviorSubject({} as any);
public yourSubject$ = this.yourSubject.asObservable();
this.yourSubject$.pipe(takeLast(1),tap((item)=>{return item})).subscribe()
Promise resolve
this.yourSubject$.subscribe((element)=>{
const item = Promise.resolve(element)
return item;
});
//or your choice
this.yourSubject$.subscribe(async (element)=> {
const item = await element;
return item;
})
I am creating a chat app, and performance is slow when a lot of messages are sent at once. The messages show up but the UI becomes unresponsive for a while. Here is a simplified version of the code and how can I fix this?
HTML:
<div class="message-notification" *ngFor="let newMessage of chatMessages"; trackBy: trackByMsgs>
<custom-notification [incomingChat]="newMessage" (dismissedEvent)="onDismiss($event)" (closedEvent)="onNotifcationClose($event)"></custom-notification>
</div>
TS:
newChatMessages: any[];
constructor(private chatService: ChatService) {}
ngOnInit() {
this.chatService.chatMsg.subscribe((msg:any) => {
if (msg) {
this.activatePopup(msg);
}
}
activatePopup(message) {
if (message.msgId !== null && message.title !== null) {
this.setTitle(message);
//If popup is already open then do not display a duplicate popup for the same message
let index = this.isPopupOpen(message);
if (index === -1) {
newChatMessages.push(message);
}else {
newChatMessages[index] = message;
}
}
}
trackByMsgs(index:number, msg:any) {
return msg.msgId && msg.title;
}
isPopUpOpen(message){
let index = -1;
if (this.newChatMessages){
index = this.newChatMessages.findIndex(
msg => msg.id === message.id && msg.title === message.title);
}
return index;
}
The best in your case is to control the angular change detection manually by using OnPush Change Detection Strategy. You should carefully use it, cause when it's on, angular will detect changes only if onChanges lifecycle is been triggered or async pipe has received a new value. It also applies to all the component children.
Then you would need to detect the changes manually by injecting the ChangeDetectorRef in your component and call the method detectChanges on it each time you want to apply your data changes to your dom.
Read this article for better understanding
Another interesting article for improving the performance of your angular app https://medium.com/swlh/angular-performance-optimization-techniques-5b7ca0808f8b
Using trackBy helps angular to memorize the loaded elements in the ngFor and update only the changed once on change detection. But your trackByMsgs returns a boolean which is not what it should return. If you adjust your trackBy to return a unique key like msg.msgId or the index of the item, you might see a difference.
I have a template
<div *ngFor="let item of list">
{{item.name}}
</div>
Then in ts code.
ngOnInit() {
const s0 = this.service.getList(this.id);
const s2 = this.service.getOthers(this.id);
combineLatest([s0, s1]).subscribe(([r0, r1]) => {
this.list = r0;
console.log('service is called');
}
}
In another place, I have a button click event to add a new item to the list.
addItemToList(item: any) {
this.service.addItem(item).subscribe(
value => {
console.log(value);
// then reload page by calling get list again
this.service.getList(this.id).subscribe(
res => this.list = res; // I want to refresh the view by this new list
console.log(res);
);
}
);
}
I am sure I added the new item successfully. But the view is not updating and the line console.log('service is called') in combineLatest was called many times. So the list is still the value when first time loading.(this.list = r0)
I can only update the view by click F5. I have tried ngZone or ChangeDetectorRef. Just not working....
It looks like you're trying to output two responses from your subscribe in
combineLatest([s0, s1]).subscribe(([r0, r1]) => {
this.list = r0;
console.log('service is called');
}
I think you need to have one response from your subscribe. Then split it after like:
combineLatest([s0, s1]).subscribe(res => {
this.list = res;
***split data***
console.log('service is called');
}
However if you want to differentiate the two observables you may have to nest their data in an object as when they are combined they will be returned as one response. Or you may not want to use combineLatest as it combines them.
I figured it out by myself. Just add take(1)
Before:
combineLatest([s0, s1]).subscribe(([r0, r1])
After:
combineLatest([s0, s1]).pipe(take(1)).subscribe(([r0, r1])
I am wanting to use ion-slides with ion-img to produce a gallery since I am thinking there may be a fair no of images. ion-img in ionic 4 uses lazy-loading to fetch the images which I think is what I want.
I am fetching the images from an Azure storage account which needs to have a dynamic (expiring) key added to it when the url is fetched.
I have managed to make this work pretty well as follows, heres the html:
<ion-slides #photoslider [options]="sliderOptions"
(ionSlideDidChange)="checkKeyandFixText()"
(ionSlidesDidLoad)="checkKeyandFixText()">
<ion-slide *ngFor="let i of userService.user.Images">
<ion-img cache="true" width="50%" [src]="i.ImageUrl + validKey"></ion-img>
</ion-slide>
</ion-slides>
which checks the key (and fixes up an index for text use with each load or image change).
The check key function is this:
checkKeyandFixText() {
console.log(this.validKey);
this.userService.getStorageURLKey('').then(() => { // trigger key update
console.log(this.validKey);
});
.... more irrelevant stuff
}
the 'validKey' used in the html is updated by the 'getStorageURLKey function, because that function fires the service that gets the key and updates an observable, which is set up in the ngOnInit - part of which includes:
this.azureKey$ = this.userService.azurekey$.subscribe((res) => {
this.validKey = res.Key;
});
But this is unreliable, depending on the state of the key and all manner of async timing possibilities.
Its been a bit of a road to here, stating with the idea that writing the 'getStorageURLKey' function would work, by providing the url, it checking if the current key is valid or not then if not getting one, and adding it back.
That function by necessity returns a Promise which the ion-img seems unable to cope with. (Here is the function for completeness:
async getStorageURLKey(url: string): Promise<string> {
const nowplus5 = addMinutes(Date.now(), 5); // 5 mins to give some leeway
console.log(nowplus5);
console.log(url);
console.log(this.azurekey);
if (!this.azurekey || isBefore( this.azurekey.Expires, nowplus5 )) {
console.log('Getting new Key');
const keyObj = await this.getAzureKeyServer().toPromise();
await this.saveAzureKeyStore(keyObj);
this.azurekey = keyObj;
console.log(this.azurekey);
return url + keyObj.Key; // Return the url with the new Key or just the key if url is blank
} else {
console.log('Key is valid till ' + this.azurekey.Expires);
console.log(this.azurekey.Key);
const rval = Promise.resolve(url + this.azurekey.Key);
return rval ; // Key is in time so return it with url
}
}
If I just call the function from inside the html:
<ion-img cache="true" width="50%" [src]="userService.getStorageURLKey(i.ImageURL) "></ion-img>
it just loops forever due to angulars retry and event loops. If I pipe the result to async:
<ion-img cache="true" width="50%" [src]="userService.getStorageURLKey(i.ImageURL) | async "></ion-img>
Then it loops over the images the requisite no of times but doesn't do anything else... sometimes, then sometimes it seems to just make no difference and loops forever.
I have also tried adding my own custom pipe instead of the async but the inbound value always comes in as 'undefined'.
Changing the html just to reference "i.ImageURl | urkKey", here's the pipe I tried:
#Pipe({
name: 'urlKey'
})
export class URLKeyPipe implements PipeTransform {
userService: UserService;
transform(value: string): any {
console.log('At Pipe');
console.log(value);
this.userService.getStorageURLKey(value).then((key) => {
console.log(key);
return key;
});
}
}
I have tried a lot of things...:-s
Fundamentally I need to add an an async key to an async url and seem to be looped up in angular. Am I approching this wrongly, or should I just go try using plain old img instead?
Any ideas? Thanks in advance.
So I just started trying to learn rxjs and decided that I would implement it on a UI that I'm currently working on with React (I have time to do so, so I went for it). However, I'm still having a hard time wrapping my head around how it actually works... Not only "basic" stuff like when to actually use a Subject and when to use an Observable, or when to just use React's local state instead, but also how to chain methods and so on. That's all too broad though, so here's the specific problem I have.
Say I have a UI where there's a list of filters (buttons) that are all clickeable. Any time I click on one of them I want to, first of all, make sure that the actions that follow will debounce (as to avoid making network requests too soon and too often), then I want to make sure that if it's clicked (active), it will get pushed into an array and if it gets clicked again, it will leave the array. Now, this array should ultimately include all of the buttons (filters) that are currently clicked or selected.
Then, when the debounce time is done, I want to be able to use that array and send it via Ajax to my server and do some stuff with it.
import React, { Component } from 'react';
import * as Rx from 'rx';
export default class CategoryFilter extends Component {
constructor(props) {
super(props);
this.state = {
arr: []
}
this.click = new Rx.Subject();
this.click
.debounce(1000)
// .do(x => this.setState({
// arr: this.state.arr.push(x)
// }))
.subscribe(
click => this.search(click),
e => console.log(`error ---> ${e}`),
() => console.log('completed')
);
}
search(id) {
console.log('search --> ', id);
// this.props.onSearch({ search });
}
clickHandler(e) {
this.click.onNext(e.target.dataset.id);
}
render() {
return (
<section>
<ul>
{this.props.categoriesChildren.map(category => {
return (
<li
key={category._id}
data-id={category._id}
onClick={this.clickHandler.bind(this)}
>
{category.nombre}
</li>
);
})}
</ul>
</section>
);
}
}
I could easily go about this without RxJS and just check the array myself and use a small debounce and what not, but I chose to go this way because I actually want to try to understand it and then be able to use it on bigger scenarios. However, I must admit I'm way lost about the best approach. There are so many methods and different things involved with this (both the pattern and the library) and I'm just kind of stuck here.
Anyways, any and all help (as well as general comments about how to improve this code) are welcome. Thanks in advance!
---------------------------------UPDATE---------------------------------
I have implemented a part of Mark's suggestion into my code, but this still presents two problems:
1- I'm still not sure as to how to filter the results so that the array will only hold IDs for the buttons that are clicked (and active). So, in other words, these would be the actions:
Click a button once -> have its ID go into array
Click same button again (it could be immediately after the first
click or at any other time) -> remove its ID from array.
This has to work in order to actually send the array with the correct filters via ajax. Now, I'm not even sure that this is a possible operation with RxJS, but one can dream... (Also, I'm willing to bet that it is).
2- Perhaps this is an even bigger issue: how can I actually maintain this array while I'm on this view. I'm guessing I could use React's local state for this, just don't know how to do it with RxJS. Because as it currently is, the buffer returns only the button/s that has/have been clicked before the debounce time is over, which means that it "creates" a new array each time. This is clearly not the right behavior. It should always point to an existing array and filter and work with it.
Here's the current code:
import React, { Component } from 'react';
import * as Rx from 'rx';
export default class CategoryFilter extends Component {
constructor(props) {
super(props);
this.state = {
arr: []
}
this.click = new Rx.Subject();
this.click
.buffer(this.click.debounce(2000))
.subscribe(
click => console.log('click', click),
e => console.log(`error ---> ${e}`),
() => console.log('completed')
);
}
search(id) {
console.log('search --> ', id);
// this.props.onSearch({ search });
}
clickHandler(e) {
this.click.onNext(e.target.dataset.id);
}
render() {
return (
<section>
<ul>
{this.props.categoriesChildren.map(category => {
return (
<li
key={category._id}
data-id={category._id}
onClick={this.clickHandler.bind(this)}
>
{category.nombre}
</li>
);
})}
</ul>
</section>
);
}
}
Thanks, all, again!
Make your filter items an Observable streams of click events using Rx.Observable.fromevent (see https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/events.md#converting-a-dom-event-to-a-rxjs-observable-sequence) - it understands a multi-element selector for the click handling.
You want to keep receiving click events until a debounce has been hit (user has enabled/disabled all filters she wants to use). You can use the Buffer operator for this with a closingSelector which needs to emit a value when to close the buffer and emit the buffered values.
But leaves the issue how to know the current actual state.
UPDATE
It seems to be far easier to use the .scan operator to create your filterState array and debounce these.
const sources = document.querySelectorAll('input[type=checkbox]');
const clicksStream = Rx.Observable.fromEvent(sources, 'click')
.map(evt => ({
name: evt.target.name,
enabled: evt.target.checked
}));
const filterStatesStream = clicksStream.scan((acc, curr) => {
acc[curr.name] = curr.enabled;
return acc
}, {})
.debounce(5 * 1000)
filterStatesStream.subscribe(currentFilterState => console.log('time to do something with the current filter state: ', currentFilterState);
(https://jsfiddle.net/crunchie84/n1x06016/6/)
Actually, your problem is about RxJS, not React itself. So it is easy. Suppose you have two function:
const removeTag = tagName =>
tags => {
const index = tags.indexOf(index)
if (index !== -1)
return tags
else
return tags.splice(index, 1, 0)
}
const addTag = tagName =>
tags => {
const index = tags.indexOf(index)
if (index !== -1)
return tags.push(tagName)
else
return tags
}
Then you can either using scan:
const modifyTags$ = new Subject()
modifyTags$.pipe(
scan((tags, action) => action(tags), [])
).subscribe(tags => sendRequest(tags))
modifyTags$.next(addTag('a'))
modifyTags$.next(addTag('b'))
modifyTags$.next(removeTag('a'))
Or having a separate object for tags:
const tags$ = new BehaviorSubject([])
const modifyTags$ = new Subject()
tags$.pipe(
switchMap(
tags => modifyTags$.pipe(
map(action => action(tags))
)
)
).subscribe(tags$)
tags$.subscribe(tags => sendRequest(tags))