How do I unsubscribe correctly from angular service results? - javascript

I have a method in the component that calls a service which returns an observable
Component Method code
public upload(file) {
this.Service.ToBase64(files[0])
.subscribe(data => (this.convertedFile = data));
}
This works fine but when I chain unsubscribe to it, it stops working.
With Unsubscribe - This does not work
public upload(file) {
this.Service.ToBase64(files[0])
.subscribe(data => (this.convertedFile = data)).Unsubscribe();
}
Service Code method
convertedFile$: Subject<string> = new Subject<string>();
ToBase64(file: any) {
const myReader = new FileReader();
myReader.onloadend = e => {
this.convertedFile$.next(myReader.result.toString().split(',')[1]);
};
myReader.readAsDataURL(file);
return this.convertedFile$.asObservable();
}
As this a subject, I would like to unsubscribe. How can I do that correctly?

You must declare a Subscription property
First in your component
import { Subscription } from 'rxjs';
Then
fileSubscription: Subscription;
And in your method
public upload(file) {
this.fileSubscription = this.Service.ToBase64(files[0])
.subscribe(data => (this.convertedFile = data));
}
In ngOnDestroy method
ngOnDestroy() {
if (this.fileSubscription) {
this.fileSubscription.unsubscribe();
}
}
Regards

The method is unsubscribe() not Unsubscribe(). But a more elegant way to get a value of observable and destroy the subscription is use the first operator like that:
import { first } from 'rxjs/operators';
public upload(file) {
this.Service.ToBase64(files[0]).pipe(first())
.subscribe(data => (this.convertedFile = data));
}

Related

Mobx observer component doesn't get update after observable change

I have the following MobX class that maintain async operation:
import { makeObservable, observable, action } from "mobx";
class AsyncAction<T, P = void> {
public isLoading = false;
public error?: unknown;
public response?: T;
constructor(
private asyncAction: (payload: P) => Promise<T>,
) {
makeObservable(this, {
error: observable,
isLoading: observable,
response: observable,
setLoading: action,
setError: action,
setResponse: action,
});
}
setLoading(isLoading: boolean) {
this.isLoading = isLoading;
}
setError(error: unknown) {
this.error = error;
}
setResponse(response: T | undefined) {
this.response = response;
}
async run(payload: P) {
try {
this.setLoading(true);
const response = await this.asyncAction(payload);
this.setResponse(response);
} catch (error) {
this.setError(error);
} finally {
this.setLoading(false);
}
}
}
export { AsyncAction };
I also have the following store that extends AsyncAction:
import api from '../api';
import { AsyncAction } from "./AsyncAction";
class StatusesStore extends AsyncAction<Record<string, Status>> {
constructor() {
super(async () => {
const { statusesMap } = await api.fetchStatusesMap();
return statusesMap;
});
makeObservable(this, {
setStatus: action,
});
}
public setStatus(name: string, status: Status) {
if (this.response) {
this.response[name] = status;
}
}
}
When I trigger from my component rootStore.statusesStore.setStatus('name', 'DONE'), the component doesn't get updated.
When open devools, I see the following:
Instead of being wrapped with observable, the object keys are plain strings. This might be a reason why changing the status string triggers nothing.
How can I fix that? What am I missing?
Since async processes are resolved in the next tick of the event loop, mobx can't track the changes after the tick. One of the solutions is to use
runInAction function after every await keyword.
Like this:
import {runInAction} from 'mobx'
async run(payload: P) {
try {
this.setLoading(true);// this is okay
const response = await this.asyncAction(payload);
// everything after "await" must be wrapped
runInAction(()=>{
this.setResponse(response);
})
} catch (error) {
this.setError(error); // wrap in runInAction
} finally {
this.setLoading(false); // wrap in runInAction
}
}
There are also a few other alternatives how you can deal with promises in combination with Mobx. For me, runInAction is the most straightforward way.
For other examples check out official documentation:
Asynchronous actions

avoid nested subscribe if there is a forkjoin inside

Here is my code in angular
this.service.save(body).subscribe(
resp => {
this.dialog.confirmation({
message: 'save object successfully!'
})
.subscribe((ok) => {
if(ok) {
this.pro.status = resp.status;
this.loadingData(resp);
const s1 = this.service.getSummary(this.id);
const s2 = this.service.getCost(this.id);
forkJoin([s1, s2]).subscribe([r1, r2]) => {
this.view = r1;
this.list = r2;
}
}
});
}
);
So there are many levels of subscribe. Not only it is ugly also the result is wrong and I can't not find it out by debugging. How can I rewrite it with rxjs operators?
You can simplify it using the RxJS operators, like the following:
// import { EMPTY, forkJoin } from 'rxjs';
// import { map, mergeMap } from 'rxjs/operators';
this.service
.save(body)
.pipe(
mergeMap((result) =>
// Merge the main observable with the dialog confirmation one..
// and map it to an object that contains the result from both observables.
this.dialog
.confirmation({ message: 'save object successfully!' })
.pipe(map((confirmed) => ({ result, confirmed })))
),
mergeMap(({ result, confirmed }) => {
if (confirmed) {
this.pro.status = result.status;
this.loadingData(result);
const s1 = this.service.getSummary(this.id);
const s2 = this.service.getCost(this.id);
return forkJoin([s1, s2]);
}
// Don't emit any value, if the dialog is not confirmed:
return EMPTY;
})
)
.subscribe(([r1, r2]) => {
this.view = r1;
this.list = r2;
});
Note: To handle the memory leaks, it's highly recommended to unsubscribe from the observable when you don't need it anymore, and this can be achieved based on your use cases, such as assigning the subscribe function result to a Subscription variable and calling unsubscribe in ngOnDestroy lifecycle hook, or using a Subject with takeUntil operator and calling next/complete functions in ngOnDestroy.
And here is how to use the unsubscribe method for example:
// import { Subscription } from 'rxjs';
#Component({...})
export class AppComponent implements OnInit, OnDestroy {
subscription: Subscription
ngOnInit(): void {
this.subscription = this.service.save(body)
// >>> pipe and other RxJS operators <<<
.subscribe(([r1, r2]) => {
this.view = r1;
this.list = r2;
});
}
ngOnDestroy() {
this.subscription.unsubscribe()
}
}
You can read more about that here: https://blog.bitsrc.io/6-ways-to-unsubscribe-from-observables-in-angular-ab912819a78f
This should be roughly equivalent:
this.service.save(body).pipe(
mergeMap(resp =>
this.dialog.confirmation({
message: 'save object successfully!'
}).pipe(
// This filter acts like your `if(ok)` statement. There's no
// else block, so if it's not okay, then nothing happens. The
// view isn't updated etc.
filter(ok => !!ok),
mapTo(resp)
)
),
tap(resp => {
this.pro.status = resp.status;
// If the following line mutates service/global state,
// it probably won't work as expected
this.loadingData(resp);
}),
mergeMap(_ => forkJoin([
this.service.getSummary(this.id),
this.service.getCost(this.id)
]))
).subscribe([view, list]) => {
this.view = view;
this.list = list;
});

Unsubscribe observable in an Angular service

I want to unsubscribe an observable in an angular service once a certain state is present. The unsubscribe should be executed within the subscription. Unfortunately the code below does not work.
#Injectable()
export class CartManagementUsecase {
private unsubscribe = new Subject();
public streamSession(): void {
this.adapter
.streamSession()
.pipe(takeUntil(this.unsubscribe))
.subscribe((session) => {
if(session.session_state === SessionState.CLOSED) {
this.unsubscribe.unsubscribe();
}
});
}
}
I would recommend you to take events untill closed is come. no subject would be required
public streamSession(): void {
this.adapter
.streamSession()
.pipe(takeWhile(session => session.session_state != SessionState.CLOSED))
.subscribe((session) => {
// do something that is required
});
}
You need to call complete on your subject for takeUntil to unsubscribe to the observable.
#Injectable()
export class CartManagementUsecase {
private unsubscribe = new Subject();
public streamSession(): void {
this.adapter
.streamSession()
.pipe(takeUntil(this.unsubscribe))
.subscribe((session) => {
if(session.session_state === SessionState.CLOSED) {
this.unsubscribe.complete();
}
});
}
}

how to fix the unsubscribe in angular

TS
tempThermometer = new BehaviorSubject<any>([]);
subscription: Subscription;
const promises = list.map(
(url: any) =>
new Promise(resolve => {
this.subscription = this.global.getData(url.link).pipe(take(1)).subscribe((res) => {
const urlArr = new Array();
urlArr.push(url);
this.tempThermometer.value.filter((data: any) => {
if (data.spinning) {
return data.spinning = urlArr.findIndex((x: any) => x.sensor === data.sensor) === -1
}
return;
});
resolve(res);
}, (err: Error) => {
return reject(err);
});
})
);
merge(...observables).subscribe((results) => {
console.log(results);
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
What I want to do here is to unsubscribe the promises, because when I click to other page it still running/fetching a data and I want it to stop when I click to other page.
the unsubscribe doesn't work. how to fix it?
The most basic way is to store the Subscription returned from a call to subscribe, and then calling the unsubscribe method on the Subscription when you leave the page (ngOnDestroy life cycle hook in Angular, more about the lifecycle hooks: here).
In your component:
ngOnInit() {
this.sub = this.something.subscribe( ... )
}
ngOnDestroy() {
this.sub.unsubscribe();
}
There are many other ways too:
Using the async pipe in your template where you need the values. It will unsubscribe automatically for you!
take operator that you used in your example will unsubscribe after N values.
takeWhile operator that will unsubscribe based on a predicate.
Here's an article discussing 6 different ways of unsubscribing: https://blog.bitsrc.io/6-ways-to-unsubscribe-from-observables-in-angular-ab912819a78f

Not able to create observable

Calling init function from another class, console.log is never called. Later, calling broadcast gives the following error:
Uncaught (in promise) TypeError: Cannot read property 'next' of
undefined
File with observable code:
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/share';
import 'rxjs/add/observable/of';
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import Deferred from './Deferred';
import * as m from '../Models/models';
let sharedServiceInstance = null;
export default class SharedService {
observable: Observable<any>;
observer: Observer<any>;
constructor() {
if(!sharedServiceInstance){
sharedServiceInstance = this;
}
return sharedServiceInstance;
}
init = () =>{
var deferred = new Deferred<any>();
if(this.observable != undefined){
deferred.resolve();
}
else{
this.observable = Observable.create((observer: Observer<any>) => {
this.observer = observer;
console.log("Observer: " + JSON.stringify(this.observer,null,4));
deferred.resolve();
}).share();
}
return deferred.promise;
}
broadcast(event: m.SharedEventModel) {
this.observer.next(event);
}
on(eventName, callback) {
return this.observable.filter((event) => {
return event.Name === eventName;
}).subscribe(callback);
}
}
File where observable is initiated and called:
import { Subject } from 'rxjs/Subject';
import SharedService from '../Services/sharedService';
import * as m from '../Models/models';
let initializeServiceInstance;
export default class InitializeService {
private sharedService = new SharedService();
public constructor(){
if(!initializeServiceInstance){
this.initialize();
initializeServiceInstance = this;
}
return initializeServiceInstance;
}
initialize =() =>{
var promise1 = this.sharedService.init()
.then(()=>{
//Debugger never reaches here
})
.catch((response)=>{
//Debugger never reaches here
var event = new m.SharedEventModel({
Name: m.EventSubjectEnum.AfterLogout
})
this.sharedService.broadcast(event);
})
}
}
NOTE: Using rxjs without angular.
In this block of code:
this.observable = Observable.create((observer: Observer<any>) => {
this.observer = observer;
console.log("Observer: " + JSON.stringify(this.observer,null,4));
}).share();
That inner function is not executed until there's a subscription. The only place subscriptions are happening is in .on, and .on is never called. So if there are no subscriptions yet, then this.observer will be undefined. Since it can be undefined, this.observer.next(event); can throw an error.
Also, if there are ever two subscriptions, then the second subscription will overwrite this.observer, thus making it so the first subscription will not get any notifications.
I think for what you're trying to do, you'll want to use a subject.
import { Subject } from 'rxjs/Subject';
// and other imports
export default class SharedService {
subject: Subject<any>
constructor() {
if(!sharedServiceInstance){
sharedServiceInstance = this;
}
return sharedServiceInstance;
}
init = () => {
this.subject = new Subject();
}
broadcast(event: m.SharedEventModel) {
this.subject.next(event);
}
// I don't recommend mixing callbacks and observables in this way
on(eventName, callback) {
return this.subject.filter((event) => {
return event.Name === eventName;
}).subscribe(callback);
}
// My recomendation would be to just return the observable
// That way the caller can decide whether they want to subscribe
// Or whether they want to do additional manipulation of the stream
//on(eventName) {
// return this.subject.filter((event) => {
// return event.Name === eventName;
// });
//}
}

Categories