Mobx observer component doesn't get update after observable change - javascript

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

Related

Function is returning wrong return

I have a problem with a function that returns wrong return instead of proper return.
Model:
export module AbsModule {
export class AbsModul{
abs: string;
state: boolean;
}
}
import { Injectable } from '#angular/core';
import { HttpClient, HttpHeaders } from '#angular/common/http';
import { environment } from '../../environments/environment';
import { httpFactory } from '#angular/http/src/http_module';
import { AbsModule } from './abs-module';
#Injectable()
export class AbsService {
private apiUrl: string = environment.apiUrl;
constructor(private http: HttpClient) { }
getAbs(): object {
return this.http.get(this.apiUrl + "/abs/download").subscribe(data => {
const returnFromFunction = this.assigneAbs(data);
console.log(returnFromFunction);
console.log(data);
return returnFromFunction;
})
}
private assigneAbs(data: object) {
return Object.assign(new AbsModule.AbsModul(), data);
}
}
Calling function:
#Component({})
export class TestClass {
constructor(
private tests: AbsService
) {}
test(): void {
console.log(this.tests.getAbs())
}}
After that I am receiving return from subscribe instead of returnFromFunction. When I am calling this function first I see the return from Subscriber from the Test function, then I see the console log from returnFromFunction and data. Return returnFromFunction does not work - it does not return this result.
Right now you just return the observable that's returned by the http.get method. You could fix this by awaiting for the response to come and then returning the resolved and processed value.
async getAbs(): Promise<object> {
const response = await this.http.get(this.apiUrl + "/abs/download").toPromise();
return this.assigneAbs(response);
}
getAbs is an asynchronous method now so you will have to treat it appropriately in your test suite.
async test(): Promise<void> {
console.log(await this.tests.getAbs());
}
The http.get(...) method returns an observable, and the subscribe method returns an object of type Subscription.
Now, the argument being passed to the subscribe method is actually a callback, a function that returns void. Hence, having such callback return an actual value, 'returnFromFunction' in your case, is meaningless.
An observable serves as a publisher notifying its observers of some new data, so a better approach would be to have the service expose an observable and have the component (as well as a tester) subscribe to it:
export class AbsService {
private apiUrl: string = environment.apiUrl;
constructor(private http: HttpClient) { }
getAbs(): Observable<any> {
return this.http.get(this.apiUrl + "/abs/download")
.map(data => this.assigneAbs(data));
}
private assigneAbs(data: object) {
return Object.assign(new AbsModule.AbsModul(), data);
}
}
export class TestClass {
constructor(
private tests: AbsService
) {}
test(): void {
this.tests.getAbs().subscribe(data => console.log(data));
}
}

How do I unsubscribe correctly from angular service results?

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

Angular Firestore: Check if data exist and update a global variable based on that

Requested behaviour:
I would like to create an AngularService which checks if a certain document exists and updates a global variable based on the result.
Current State
The function checks the existence of the document successfully. It also updates the global variable in the if/else statement.
issue
Even, the first part works well, it always returns "undefined".
How can I fix that? Is it related to function scope?
My Service:
export class ProfileFollowService {
//global variable which should be updated
followState: boolean;
constructor(private angularFirestore: AngularFirestore) { }
checksFollow(followingID: string, followerID: string): boolean {
const followDoc =
this.angularFirestore.collection(`users/${followingID}/following`).doc(followerID).ref;
followDoc.get().then((doc) => {
if (doc.exists) {
this.followState = true;
} else {
this.followState = false;
}
});
return this.followState;
}
}
followDoc.get() is async function that returns promise. In order to return updated this.followState you have to wait for then
one way to do this is to use async / await
async checksFollow(followingID: string, followerID: string): boolean {
const followDoc =
this.angularFirestore.collection(`users/${followingID}/following`).doc(followerID).ref;
return followDoc.get().then((doc) => {
if (doc.exists) {
this.followState = true;
} else {
this.followState = false;
}
return this.followState;
});
}
In other part of your code where you call checksFollow you can put keyword await and wait for the response.
async someMethodToCallChecksFollow() {
const result = await this.checksFollow();
console.log(result);
}
If you want to use the response in your html, I would suggest changing followState from primitive boolean to BehaviorSubject<boolean> and then call this.followState.next(true)
For example:
export class YourService {
public followState = new BehaviorSubject<boolean>(false);
async checksFollow(followingID: string, followerID: string): boolean {
const followDoc =
this.angularFirestore.collection(`users/${followingID}/following`).doc(followerID).ref;
return followDoc.get().then((doc) => {
if (doc.exists) {
this.followState.next(true);
} else {
this.followState.next(false);
}
return this.followState.getValue();
});
}
}
And then in your html you can use async pipe.
<div *ngIf="yourServiceInstance.followState | async">It is true</div>

Inconsitant mergeMap behaviour

I am currently working on a file uploading method which requires me to limit the number of concurrent requests coming through.
I've begun by writing a prototype to how it should be handled
const items = Array.from({ length: 50 }).map((_, n) => n);
from(items)
.pipe(
mergeMap(n => {
return of(n).pipe(delay(2000));
}, 5)
)
.subscribe(n => {
console.log(n);
});
And it did work, however as soon as I swapped out the of with the actual call. It only processes one chunk, so let's say 5 out of 20 files
from(files)
.pipe(mergeMap(handleFile, 5))
.subscribe(console.log);
The handleFile function returns a call to my custom ajax implementation
import { Observable, Subscriber } from 'rxjs';
import axios from 'axios';
const { CancelToken } = axios;
class AjaxSubscriber extends Subscriber {
constructor(destination, settings) {
super(destination);
this.send(settings);
}
send(settings) {
const cancelToken = new CancelToken(cancel => {
// An executor function receives a cancel function as a parameter
this.cancel = cancel;
});
axios(Object.assign({ cancelToken }, settings))
.then(resp => this.next([null, resp.data]))
.catch(e => this.next([e, null]));
}
next(config) {
this.done = true;
const { destination } = this;
destination.next(config);
}
unsubscribe() {
if (this.cancel) {
this.cancel();
}
super.unsubscribe();
}
}
export class AjaxObservable extends Observable {
static create(settings) {
return new AjaxObservable(settings);
}
constructor(settings) {
super();
this.settings = settings;
}
_subscribe(subscriber) {
return new AjaxSubscriber(subscriber, this.settings);
}
}
So it looks something like this like
function handleFile() {
return AjaxObservable.create({
url: "https://jsonplaceholder.typicode.com/todos/1"
});
}
CodeSandbox
If I remove the concurrency parameter from the merge map function everything works fine, but it uploads all files all at once. Is there any way to fix this?
Turns out the problem was me not calling complete() method inside AjaxSubscriber, so I modified the code to:
pass(response) {
this.next(response);
this.complete();
}
And from axios call:
axios(Object.assign({ cancelToken }, settings))
.then(resp => this.pass([null, resp.data]))
.catch(e => this.pass([e, null]));

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