I have the following code that dispatch 3 actions:
deleteLineFailed
showConfirmationMessage
Wait 2s
hideConfirmationMessage
For some reasons, the only way I was able to make it work is in the reverse order, what is it I'm doing wrong?
const deleteLineEpic = (action$, store) =>
action$.ofType(types.DELETE_LINE_REQUEST)
.flatMap((action) => {
return Observable.of(hideConfirmationMessage(action.line.productID))
.delay(2000)
.merge(
Observable.of(deleteLineFailure(action.line.productID)),
Observable.of(showConfirmationMessage(action.line.productID))
);
}
});
I think the following happens:
deleteLine -----X------------
showConfirmation -------Y----------
hideConfirmation --------- 2s -----Z
merge -----X-Y- 2s -----Z
All 3 streams get merged, and the one with the delay emits after two seconds while the other emit the action immediately.
So it might be better to do this:
const deleteLineEpic = (action$, store) =>
action$.ofType(types.DELETE_LINE_REQUEST)
.flatMap((action) => {
return Observable.merge(
Observable.of(deleteLineFailure(action.line.productID),
showConfirmationMessage(action.line.productID)),
Observable.of(hideConfirmationMessage(action.line.productID))
.delay(2000)
)}
);
Here the actions in the first Observable.of get immediately emitted after each other, while hide actions is emitted later on.
Related
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.
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.
I have a use case where I need to wait for a sequence of actions before I dispatch another using Redux Observables. I've seen some similar questions but I cannot fathom how I can use these approaches for my given use case.
In essence I want to do something like so:
action$
.ofType(PAGINATION_CLICKED) // This action occurred.
.ofType(FETCH_SUCCESS) // Then this action occurred after.
.map(() => analyticsAction()); // Dispatch analytics.
I would also like to cancel and start that sequence over again if another action of type FETCH_ERROR fires for example.
Great question. The important point is that action$ is a hot/multicast stream of all actions as they are dispatched (it's a Subject). Since it's hot we can combine it multiple times and they'll all be listening to the same stream of actions.
// uses switchMap so if another PAGINATION_CLICKED comes in
// before FETCH_SUCCESS we start over
action$
.ofType(PAGINATION_CLICKED)
.switchMap(() =>
action$.ofType(FETCH_SUCCESS)
.take(1) // <-------------------- very important!
.map(() => analyticsAction())
.takeUntil(action$.ofType(FETCH_ERROR))
);
So every time we receive PAGINATION_CLICKED we'll start listening to that inner Observable chain that listens for a single FETCH_SUCCESS. It's important to have that .take(1) because otherwise we'd continue to listen for more than one FETCH_SUCCESS which might cause strange bugs and even if not is just generally best practice to only take what you need.
We use takeUntil to cancel waiting for FETCH_SUCCESS if we receive FETCH_ERROR first.
As a bonus, if you decide you want also to do some analytics stuff based on the error too, not only start over, you can use race to indeed race between the two streams. First one to emit, wins; the other is unsubscribed.
action$
.ofType(PAGINATION_CLICKED)
.switchMap(() =>
Observable.race(
action$.ofType(FETCH_SUCCESS)
.take(1)
.map(() => analyticsAction()),
action$.ofType(FETCH_ERROR)
.take(1)
.map(() => someOtherAnalyticsAction())
)
);
Here's the same thing, but using race as an instance operator instead of the static one. This is a stylistic preference you can choose. They both do the same thing. Use whichever one is more clear to you.
action$
.ofType(PAGINATION_CLICKED)
.switchMap(() =>
action$.ofType(FETCH_SUCCESS)
.map(() => analyticsAction())
.race(
action$.ofType(FETCH_ERROR)
.map(() => someOtherAnalyticsAction())
)
.take(1)
);
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$)
Hi there I have a onChange callback in one of the React components that dispatches an action several times through a map call like this:
onChange: (items, newRatio) => {
items.map( item => {
dispatch(itemActions.updateStart({
...item,
adjusted_ratio: _.round(item.adjusted_ratio + newRatio, 1),
}))
})
}
and I have a Saga for the "items" like so:
// Updating an Item
function* watchUpdate() {
while(true) {
const { record: unsavedItem, } = yield take(itemTypes.ITEMS_UPDATE_START);
const task = yield fork( updateItemDbCrud, unsavedItem )
}
}
function* updateItemDbCrud(unsavedItem) {
const savedItem = yield call( api.update, unsavedItem );
const result = yield put ( itemActions.updateSuccess(savedItem) )
}
export default [watchUpdate]
In other words, I expected that whenever the ITEMS_UPDATE_START action gets dispatched, it forks a new updateItemDbCrud and proceeds to do some API work, but I notice that only the first of the sequence of dispatches goes through. Am I using the fork wrong?
Thank you!
It's a known issue (https://github.com/yelouafi/redux-saga/issues/50) and it has to do with the nature of promises and its use in Redux Sagas core.
It's been resolved in version 0.6.
if you want to learn more about what caused the issue I recommend reading the above github issue and also Jake Archibalds article on tasks, microtasks, queues and schedules.
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/