NGRX - How to subscribe to multiple requests that update a list, but prevent it from loading multiple times on the first load? - ngrx

I'm working with Angular-NGRX and i have 3 components that can modify the data of a list. In the main component I listen for all the changes to update the listing and it works. The problem is that the first load subscribes me 3 times.
This is the ngOnInit of the main component:
ngOnInit(): void {
// Change order
this.store.select(state => state.shared.orderBy).pipe(
takeUntil(this.onDestroy$),
tap(resp => {
this.orderBy = resp;
this._cmsFunctions.deselectAll('Resource');
}),
switchMap(resp => this._dynamicComponentsService.loadResources(this.itemsToShow, this.tabCounter, this.orderBy, this.filters, this.searchTerm, this.company, this.workgroup, (this.paramCode) ? this.paramCode : 0))
).subscribe();
// Change filters
this.store.select(state => state.shared.filters).pipe(
takeUntil(this.onDestroy$),
tap(resp => {
this.filters = resp;
this._cmsFunctions.deselectAll('Resource');
}),
switchMap(resp => this._dynamicComponentsService.loadResources(this.itemsToShow, this.tabCounter, this.orderBy, this.filters, this.searchTerm, this.company, this.workgroup, (this.paramCode) ? this.paramCode : 0))
).subscribe();
// Change search term
this.store.select(state => state.shared.searchTerm).pipe(
takeUntil(this.onDestroy$),
tap(resp => {
this.searchTerm = resp;
this._cmsFunctions.deselectAll('Resource');
}),
switchMap(resp => this._dynamicComponentsService.loadResources(this.itemsToShow, this.tabCounter, this.orderBy, this.filters, this.searchTerm, this.company, this.workgroup, (this.paramCode) ? this.paramCode : 0))
).subscribe();
}
And all i need is that when starting it only subscribes once:
enter image description here
How can I improve this code?
Thanks!

You shouldn't be making calls to a data service from the component, and certainly not based on selector calls. You should be dispatching an Action which is captured and handled by an Effect. You can use dataLoading and dataLoaded flags in your store, for example, to prevent multiple loads.

Related

How to limit mergeMap inner subscriptions to the N latest or a sliding window queue

I have a source stream merged from two streams. When the source stream emit event I'd like to call a subscription function Meteor.subscribe and keep it open, so I use mergeMap. When subscription is ready I pipe to another mergeMap to populate the data. It works well until I do 100 clicks and memory consumption is skyrockets. The question is, how is it possible to limit mergeMap, not to the first N subscriptions by concurrent: Number, but to the N recent ones, like a sliding window?
function paginationCache$(): Observable<any> {
return merge(this.pageParamsChanged$, this.routerParamsChanged$)
.pipe(
mergeMap((newParams) => {
// First merge map subscribes to data and un subscribes when second merge map unsubscribes
return Observable.create((observer: Subscriber<any>) => {
let subscription = Meteor.subscribe('my/best/data/sub', newParams,
() => {
observer.next(subscription);
observer.complete();
});
});
}),
mergeMap((subscription: any) => {
// second subscription is just populating the data
return Observable.create((observer: Subscriber<Meteor.Error | any>) => {
const collection = new Mongo.Collection(subscription.collectionName);
const { selector, options } = this.mongoParams();
collection.find(selector, options).dataChanges((data) => {
observer.next({ data });
});
return () => {
subscription.stop();
};
});
})
);
}
I'd like to give more detailed explanation what happening in that code.
In my example, source stream (the merge before pipe) it's never completes as long as I click button in my web interface, so it emits changes as I click next or previous button in my interface. First mergeMap gets changes from the source stream and sends them to backend API (which also has conflicted naming publication/subscription). So when data available on the client I call observer.next(subscription) to move to the second mergeMap, but I can't destroy or stop meteor's subscription. Two reasons: 1. I'd like to get realtime changes to selected data, 2. if I stop meteor's subscription, data on the client side will be removed. So, now second mergeMap it continuously updates selected data if it was updated on the server.
So after each UI button click (next, previous) I have new chain of subscriptions. It is okey if the original data table not big (1000 records) and I just clicked couple times. But, I can have more than that 30000 and I can click my buttons many times.
So, the idea is to make mergeMap like a limited size queue that holds just last N subscriptions, but queue is changing all the time I click the button.
LAST EDIT: working code:
function paginationCache$(): Observable<any> {
const N = 3;
const subscriptionsSubject = new Subject();
return merge(this.pageParamsChanged$, this.routerParamsChanged$)
.pipe(
mergeMap((newParams) => {
// First merge map subscribes to data and un subscribes when second merge map unsubscribes
subscriptionsSubject.next();
return Observable.create((observer: Subscriber<any>) => {
let subscription = Meteor.subscribe('mu/best/data/sub', newParams,
() => {
observer.next(subscription);
observer.complete();
});
});
}),
mergeMap((subscription: any) => {
// second subscription is just populating the data
return Observable.create((observer: Subscriber<Meteor.Error | any>) => {
const collection = new Mongo.Collection(subscription.collectionName);
const { selector, options } = this.mongoParams();
collection.find(selector, options).dataChanges((data) => {
observer.next({ data });
});
return () => {
subscription.stop();
};
}).pipe(
takeUntil(subscriptionsSubject
.pipe(
take(N),
filter((_, idx) => idx === N - 1)
)
)
);
})
);
}
Without considering your snippet, here's how I'd go about this:
not to the first N subscriptions by concurrent: Number, but to the N recent ones, like a sliding window
If I understood correctly, you'd want something like this(assuming N = 3):
N = 3
Crt | 1 | 2 | 3 |
Subscriptions | S1 | S2 | S3 |
When Crt = 4
Crt | 2 | 3 | 4 |
Subscriptions | S2 | S3 | S4 |
If that's the case, here's how I'd solve it:
const subscriptionsSubject = new Subject();
src$.pipe(
mergeMap(
data => (new Observable(s => {/* ... */ subscriptionsSubject.next(null) /* Notify about a new subscription when it's the case */ }))
.pipe(
takeUntil(
subscriptionsSubject.pipe(
take(N), // After `N` subscriptions, it will complete
filter((_, idx) => idx === N - 1) // Do not want to complete immediately, but only when exactly `N` subscriptions have been reached
)
)
)
)
)
I have two ideas here:
You're not completing the second inner Observable. I guess this shouldn't be the source of your problem but it's better to complete observers if you can:
return () => {
subscription.stop();
observer.complete();
};
You can use bufferCount to make a sliding window of Observables and then subscribe to them with switchMap(). Something along these lines:
import { of, range } from 'rxjs';
import { map, bufferCount, switchMap, shareReplay, tap } from 'rxjs/operators';
range(10)
.pipe(
// turn each value to an Observable
// `refCount` is used so that when `switchMap` switches to a new window
// it won't trigger resubscribe to its sources and make more requests.
map(v => of(v).pipe(shareReplay({ refCount: false, bufferSize: 1 }))),
bufferCount(3, 1),
tap(console.log), // for debugging purposes only
switchMap(sourcesArray => merge(...sourcesArray)),
)
.subscribe(console.log);
Live demo: https://stackblitz.com/edit/rxjs-kuybbs?devtoolsheight=60
I'm not completely sure this simulates your use-case but I tried to include also shareReplay so that it won't trigger multiple Meteor.subscribe calls for the same Observable. I'd have to have a working demo of your code to test it myself.

Where should the "id" of the new data object be created/assigned?

I'm building a web app using Angular 7, and want to use #ngrx/store and #ngrx/effects.
Disclaimer: I'm new to these technologies.
In a typical client-server application, I would think it makes sense to allow the database to assign the id value, which would then be passed back to the client in the HTTP response.
But given that API calls are supposed to happen in side effects when using NGRX, what's the proper way to update the new object in the state with the id returned in the http response?
My understanding is that side effects are not supposed to manipulate state. That's the whole reason they're called side effects.
What's the proper way to solve this problem?
Some possibilities:
You can let the client generate there own ID's.
You can only add entities to the store when the server roundtrip is finished.
This can be done by returning the whole object back from the server, or to append the id to the entity in an effect:
#Effect()
save = this.actions.pipe(
ofType(Foo),
exhaustMap(({ payload }) =>
this.service.post(payload).pipe(
map(
result =>
new FooSuccess({
...payload,
id: result,
})
),
catchError(() => of(new FooFailed()))
)
)
);
State manipulations are indeed done via reducers, if you can somehow link the new entity in the store with the returned payload you could update the id via the reducer
#Effect()
save = this.actions.pipe(
ofType(Foo),
exhaustMap(({ payload }) =>
this.service.post(payload).pipe(
map(
result =>
new FooSuccess({
reference: payload.reference,
id: result,
})
),
catchError(() => of(new FooFailed()))
)
)
);
// and in the reducer
return {
...state,
// mutate found entity, else just return the entity in the store
entities: this.state.entities.map(p => p.reference === action.reference ? mutateEntityHere : p)
}
My preferences would be either the first or second option.

url not defined. Refactor to new versions of angularfire, angular

I have refactor problems because my code dosnt work to the new versions of angular and angularfire.
Error
The line: upload.url = uploadTask.snapshot.downloadURL; is undefined.
Code
uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED,
// three observers
// 1.) state_changed observer
(snapshot) => {
// upload in progress
upload.progress = (uploadTask.snapshot.bytesTransferred / uploadTask.snapshot.totalBytes) * 100;
console.log(upload.progress);
},
// 2.) error observer
(error) => {
// upload failed
console.log(error);
},
// 3.) success observer
(): any => {
upload.url = uploadTask.snapshot.downloadURL; //?!?!UNDEFINED
upload.name = upload.file.name;
this.saveFileData(upload);
}
);
Questions
I had tried different solutions from stackoverflow but it dosnt really work. Most of the example is also more about how to retrieve the image but i want to set the variable upload.url to a value.
Another question:
I'm new to angular and web. Will it take long time to change it to firestore? The code is based on realtime firebase.
To get the downloadURL, you have to call the getDownloadURL() method of the Storage Reference Object.
Try this:
uploadTask.snapshot.ref.getDownloadURL()
.subscribe(url => console.log(url))

Data validation using RxJS

I have the following function that validates that rangeFrom is not superior to rangeTo and that the rangeFrom does not already exist in the list of ranges.
How can rewrite this using RxJS?
const isTagAlreadyExist = (tags, currentTag) => _(tags)
.filter(x => x.id !== currentTag.id)
.some(x => _.inRange(currentTag.rangeTo, x.rangeFrom, x.rangeTo))
.value();
const validateRangeFrom = (tags, currentTag) => {
const errors = {};
if (isNumeric(currentTag.rangeFrom)) {
if (!_.inRange(currentTag.rangeFrom, 0, currentTag.rangeTo)) {
errors.rangeFrom = 'FROM_TAG_CANNOT_BE_GREATER_THAN_TO_TAG';
} else if (isTagAlreadyExist(tags, currentTag)) {
errors.rangeFrom ='TAG_ALREADY_EXISTS';
}
}
return {
errors
};
};
The question is: what parts do you want to rewrite to rxjs? Those are two pure functions that run synchronously from what I can see, I do not really see much a usecase for rxjs here - of course you could always utilize your functions within an rxjs stream:
const validateRangeFrom$ = (tags, currentTag) => {
return Observable.of(currentTag)
.map(tag => validateRangeFrom(tags, tag));
}
validateRangeFrom$(myTags, currentTag)
.subscribe(errors => console.log(errors));
But as you can see, this does not make much sense if you simply wrap it inside a stream, the essence of useful reactive programming is, that everything is reactive, not just some small parts, so for your example, you should start with having tags$ and currentTag$ as observables - let's assume that you have that, then you could do something like:
const tags$: Observable<ITag[]>... // is set somewhere, and emits a new array whenever it is changed
const currentTag$: Observable<ITag>... // is set somewhere and emits the tag whenever a new currentTag is set
const validateRangeFrom$ = Observable
.combineLatest(tags$, currentTag$, (tags, tag) => ({tags, tag}))
.map(({tags, tag}) => validateRangeFrom(tags, tag));
validateRangeFrom$.subscribe(errors => console.log(errors));
This will automatically trigger the validation for you whenever a new tags-array is emitted or a new currentTag is selected/set - but again: your validation-method is kept the same - as even in reactive programming you have to do validation and logic-operations at some point, the reactive part usually just concerns the flow of the data (see: tags$ and currentTag$)

Rxjs: add data to elements of array returned from http response

Following this question: Add data to http response using rxjs
I've tried to adapt this code to my use case where the result of the first http call yields an array instead of a value... but I can't get my head around it.
How do I write in rxjs (Typescript) the following pseudo code?
call my server
obtain an array of objects with the following properties: (external id, name)
for each object, call another server passing the external id
for each response from the external server, obtain another object and merge some of its properties into the object from my server with the same id
finally, subscribe and obtain an array of augmented objects with the following structure: (external id, name, augmented prop1, augmented prop2, ...)
So far the only thing I was able to do is:
this._appService
.getUserGames()
.subscribe(games => {
this._userGames = _.map(games, game => ({ id: game.id, externalGameId: game.externalGameId, name: game.name }));
_.forEach(this._userGames, game => {
this._externalService
.getExternalGameById(game.externalGameId)
.subscribe(externalThing => {
(<any>game).thumbnail = externalThing.thumbnail;
(<any>game).name = externalThing.name;
});
});
});
Thanks in advance
I found a way to make it work. I'll comment the code to better explain what it does, especially to myself :D
this._appService
.getUserGames() // Here we have an observable that emits only 1 value: an any[]
.mergeMap(games => _.map(games, game => this._externalService.augmentGame(game))) // Here we map the any[] to an Observable<any>[]. The external service takes the object and enriches it with more properties
.concatAll() // This will take n observables (the Observable<any>[]) and return an Observable<any> that emits n values
.toArray() // But I want a single emission of an any[], so I turn that n emissions to a single emission of an array
.subscribe(games => { ... }); // TA-DAAAAA!
Don't use subscribe. Use map instead.
Can't test it, but should look more like this:
this._appService
.getUserGames()
.map(games => {
this._userGames = _.map(games, game => ({ id: game.id, externalGameId: game.externalGameId, name: game.name }));
return this._userGames.map(game => { /* this should return an array of observables.. */
return this._externalService
.getExternalGameById(game.externalGameId)
.map(externalThing => {
(<any>game).thumbnail = externalThing.thumbnail;
(<any>game).name = externalThing.name;
return game;
});
});
})
.mergeAll()
.subscribe(xx => ...); // here you can subscribe..

Resources