BehaviorSubject observable triggers many times - javascript

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;
})

Related

Observable from Subject emits first value as null

I'm trying to have an observable that is based on a Subject, so that it gets updated every time the SUbject emits.
For some reason, when trying to fetch the value of the observable, first value is always null, and rest contain values. I shall expect to retrieve the latest emitted value of the observable. I've tried with all kind of SUbjects. What is what I'm missing?
...
activeBookings$: Subject<Booking[]> = new BehaviorSubject<Booking[]>(null);
activeBooking$: Observable<Booking> = this.activeBookings$.pipe(
filter((bookings) => bookings?.length > 0),
mergeMap((bookings) => this.getBookingById(bookings?.[0]?.id)),
share()
);
i = 0;
constructor(
public http: HttpClient,
public store: Store,
public asyncPipe: AsyncPipe,
public bookingStoreService: BookingStoreService
) {
this.getBookings().subscribe((bookings) => {
this.bookings$.next(bookings);
});
this.getActiveBookings().subscribe((bookings) => {
this.activeBookings$.next(bookings);
});
this.getPreviousBookings().subscribe((bookings) => {
this.previousBookings$.next(bookings);
});
}
...
Here is where I try to obtain the latest value:
...
const booking: Booking = this.asyncPipe.transform(
this.bookingService.activeBooking$
);
booking.status = status; //error: booking is null
...
You are using a BehaviorSubject here with an initial value of null:
activeBookings$: Subject<Booking[]> = new BehaviorSubject<Booking[]>(null);
You could initialise it with an empty array instead for exemple:
activeBookings$: Subject<Booking[]> = new BehaviorSubject<Booking[]>([]);
It would be more consistent type wise else your real type is:
activeBookings$: Subject<Booking[] | null> = new BehaviorSubject<Booking[] | null>(null);
Why are you getting null ? A BehaviorSubject needs to be initialised with a value and that is probably why the first value you get is null. A plain Subject doesn't need to contain an initial value and you would probably not get null
activeBookings$: Subject<Booking[]> = new Subject<Booking[]>()
A cleaner way to obtain the latest value without subscribing in your other service would be to leverage the features of the BehaviorSubject and the fact that calling activeBooking$.value makes you retrieve the latest value of the BehaviorSubject. Transform your activeBooking$ field so that it is a BehaviorSubject and when subscribing to the result of getActiveBookings(), you could do the computing that is currently in the pipe of the activeBooking$ definition there and call next on activeBooking$ with the result and expose a getter for your other service:
get activeBooking(): Booking {
return this.activeBooking$.value
}

How to improve rendering performance with *ngFor in Angular

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.

Add streams dynamically to combined stream (eg forkJoin)

My function (lets call it myFunction) is getting an array of streams (myFunction(streams: Observable<number>[])). Each of those streams produces values from 1 to 100, which acts as a progress indicator. When it hits 100 it is done and completed. Now, when all of those observables are done I want to emit a value. I could do it this way:
public myFunction(streams: Observable<number>[]) {
forkJoin(streams).subscribe(_values => this.done$.emit());
}
This works fine, but imagine following case:
myFunction gets called with 2 streams
one of those streams is done, second one is still progressing
myFunction gets called (again) with 3 more streams (2nd one from previous call is still progressing)
I'd like to somehow add those new streams from 3rd bullet to the "queue", which would result in having 5 streams in forkJoin (1 completed, 4 progressing).
I've tried multiple approaches but can't get it working anyhow... My latest approach was this:
private currentProgressObs: Observable<any> | null = null;
private currentProgressSub: Subscription | null = null;
public myFunction(progressStreams: Observable<number>[]) {
const isUploading = this.cumulativeUploadProgressSub && !this.cumulativeUploadProgressSub.closed;
const currentConcatObs = this.currentProgressObs?.pipe(concatAll());
const currentStream = isUploading && this.currentProgressObs ? this.currentProgressObs : of([100]);
if (this.currentProgressSub) {
this.currentProgressSub.unsubscribe();
this.currentProgressSub = null;
}
this.currentProgressObs = forkJoin([currentStream, ...progressStreams]);
this.currentProgressSub = this.currentProgressObs.subscribe(
_lastProgresses => {
this._isUploading$.next(false); // <----- this is the event I want to emit when all progress is completed
this.currentProgressSub?.unsubscribe();
this.currentProgressSub = null;
this.currentProgressObs = null;
},
);
}
Above code only works for the first time. Second call to the myFunction will never emit the event.
I also tried other ways. I've tried recursion with one global stream array, in which I can add streams while the subscription is still avctive but... I failed. How can I achieve this? Which operator and in what oreder should I use? Why it will or won't work?
Here is my suggestion for your issue.
We will have two subjects, one to count the number of request being processed (requestsInProgress) and one more to mange the requests that are being processed (requestMerger)
So the thing that will do is whenever we want to add new request we will pass it to the requestMerger Subject.
Whenever we receive new request for processing in the requestMerger stream we will first increment the requestInProgress counter and after that we will merge the request itself in the source observable. While merging the new request/observable to the source we will also add the finalize operator in order to track when the request has been completed (reached 100), and when we hit the completion criteria we will decrement the request counter with the decrementCounter function.
In order to emit result e.g. to notify someone else in the app for the state of the pending requests we can subscribe to the requestsInProgress Subject.
You can test it out either here or in this stackBlitz
let {
interval,
Subject,
BehaviorSubject
} = rxjs
let {
mergeMap,
map,
takeWhile,
finalize,
first,
distinctUntilChanged
} = rxjs.operators
// Imagine next lines as a service
// Subject responsible for managing strems
let requestMerger = new Subject();
// Subject responsible for tracking streams in progress
let requestsInProgress = new BehaviorSubject(0);
function incrementCounter() {
requestsInProgress.pipe(first()).subscribe(x => {
requestsInProgress.next(x + 1);
});
}
function decrementCounter() {
requestsInProgress.pipe(first()).subscribe(x => {
requestsInProgress.next(x - 1);
});
}
// Adds request to the request being processed
function addRequest(req) {
// The take while is used to complete the request when we have `value === 100` , if you are dealing with http-request `takeWhile` might be redudant, because http request complete by themseves (e.g. the finalize method of the stream will be called even without the `takeWhile` which will decrement the requestInProgress counter)
requestMerger.next(req.pipe(takeWhile(x => x < 100)));
}
// By subscribing to this stream you can determine if all request are processed or if there are any still pending
requestsInProgress
.pipe(
map(x => (x === 0 ? "Loaded" : "Loading")),
distinctUntilChanged()
)
.subscribe(x => {
console.log(x);
document.getElementById("loadingState").innerHTML = x;
});
// This Subject is taking care to store or request that are in progress
requestMerger
.pipe(
mergeMap(x => {
// when new request is added (recieved from the requestMerger Subject) increment the requrest being processed counter
incrementCounter();
return x.pipe(
finalize(() => {
// when new request has been completed decrement the requrest being processed counter
decrementCounter();
})
);
})
)
.subscribe(x => {
console.log(x);
});
// End of fictional service
// Button that adds request to be processed
document.getElementById("add-stream").addEventListener("click", () => {
addRequest(interval(1000).pipe(map(x => x * 25)));
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.6.6/rxjs.umd.min.js"></script>
<div style="display:flex">
<button id="add-stream">Add stream</button>
<h5>Loading State: <span id="loadingState">false</span> </h5>
</div>
Your problem is that each time your call your function, you are creating a new observable. Your life would be much easier if all calls of your function pushed all upload jobs through the same stream.
You can achieve this using a Subject.
I would suggest you push single "Upload Jobs" though a simple subject and design an observable that emits the state of all upload jobs whenever anything changes: A simple class that offers a createJob() method to submit jobs, and a jobs$ observable to reference the state:
class UploadService {
private jobs = new Subject<UploadJob>();
public jobs$ = this.jobs.pipe(
mergeMap(job => this.processJob(job)),
scan((collection, job) => collection.set(job.id, job), new Map<string, UploadJob>()),
map(jobsMap => Array.from(jobsMap.values()))
);
constructor() {
this.jobs$.subscribe();
}
public createJob(id: string) {
this.jobs.next({ id, progress: 0 });
}
private processJob(job: UploadJob) {
// do work and return observable that
// emits updated status of UploadJob
}
}
Let's break it down:
jobs is a simple subject, that we can push "jobs" through
createJob simply calls jobs.next() to push the new job through the stream
jobs$ is where all the magic happens. It receives each UploadJob and uses:
mergeMap to execute whatever function actually does the work (I called it processJob() for this example) and emits its values into the stream
scan is used to accumulate these UploadJob emissions into a Map (for ease of inserting or updating)
map is used to convert the map into an array (Map<string, UploadJob> => UploadJob[])
this.jobs$.subscribe() is called in the constructor of the class so that jobs will be processed
Now, we can easily derive your isUploading and cumulativeProgress from this jobs$ observable like so:
public isUploading$ = this.jobs$.pipe(
map(jobs => jobs.some(j => j.progress !== 100)),
distinctUntilChanged()
);
public progress$ = this.jobs$.pipe(
map(jobs => {
const current = jobs.reduce((sum, j) => sum + j.progress, 0) / 100;
const total = jobs.length ?? current;
return current / total;
})
);
Here's a working StackBlitz demo.

Get value of current Observable state

So I am trying to get the value (the email) of my Observable<firebase.User>. I know there is something called BehaviourSubject, but I cannot use it since firebase.User requires an Observable, it seems.
retrieveUserData(){
let emailVal = "";
this.userData.subscribe(
{
next: x => emailVal += x.email && console.log(x.email),
error: err => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
}
);
return emailVal;
}
So my goal is to get x.email in the emailVal let, in order to pass it & display, for example.
The problem is, that I am getting an (by logging the whole method retrieveUserData()), but the console.log(x.email) always returns the value I am looking for.
Why is that & is there a way to get the value & store it in a string, let or something else?
It is because Observables are async. It means when you run subscribe method of the observable, it runs the command without blocking the current runtime. Also, you are assigning the value of emailVal when the observable is run but you return the value without waiting for the assignment to be happened.
What you can do?
You can keep a global variable to keep the email globally and use that variable to display the email in the html side.
#Component({
selector: "my-app",
// See here, I used emailVal to display it
template: "<span>{{emailVal}}</span>",
styles: [""]
})
export class TestComponent {
emailVal = "";
ngOnInit(): void {
this.retrieveUserData();
}
retrieveUserData(): void {
this.userData.subscribe(
x => this.handleData(x.email),
err => console.error("Observer got an error: " + err)
);
}
handleData(email) {
// Here, we assign the value of global variable (defined in class level)
this.emailVal = email;
console.log(email);
}
}
You can use rxjs library in such a way to make the observable return the value and return the observable in the method as below
retrieveUserData(): Observable<firebase.User> {
return this.userData.pipe(
map(x => x.email)
);
}
And in html side, using async pipe (as an example):
<span>{{retrieveUserData() | async}}</span>
async pipe will subscribe to observable and wait for it to complete and then take the value and put it in the value of span. You can check this StackBlitz example to understand this method deeply.

Rxjs ReplaySubject initializing with 'null' value before being emitted - Angular 7

I want to create a subject without initial value.
I have created a ReplaySubject that buffers only the last value that been emitted.
The problem is that on the subscription is giving a null as initial value before it's being emitted.
My service:
private _replaySubjects = new ReplaySubject<Project>(1);
public replaySubjectAsObservable$ = this._replaySubjects.asObservable();
My component:
this.subjectSub = someService._projectList$.subscribe(value => {
console.log(value)
});
Rxjs version - 6.5.2
This won't emit any value until a significant value is emitted with this._replaySubject.next(newProject).
It has the added benefit that you can access the last emitted value outside of a subscription with this._replaySubject.getValue().
private _replaySubjects = new BehaviorSubject<Project>(null);
public replaySubjectAsObservable$ = this._replaySubjects.asObservable().pipe(skipWhile(project => project === null || project === undefined));;

Categories

Resources