RXJS how to optimize a ForEach in a nested subscribe? - rxjs

I'm rather new to observables and still learning and i need some help.
I have a slider with some campaigns and for each campaign i have a producer which i need get and set to the campaign.
I know that using pipe, merge and MergeMap here is probably some of the solution but i can't seem to get it to work, does anyone have an idea for me?
This is what i currently have.
fetchCampaigns() {
this.loading = true;
this.campaignService
.getCampaigns(
this.campaignAmount,
this.sorting,
this.desc
)
.subscribe(
campaignListDto => {
this.campaigns = campaignListDto;
this.campaigns.map(campaign => {
this.producerService.getProducerByName(campaign.producer_name)
.subscribe((producer) => {
campaign.producer = producer
campaign.producer_name
})
})
this.loading = false;
this.fetchProducerMinPrices();
this.fetchProducerTotalCampaigns();
})
};

Try:
fetchCampaigns() {
this.loading = true;
this.campaignService.getCampaigns(this.campaignAmount,this.sorting,this.desc).pipe(
switchMap(campaignListDto => forkJoin(campaignListDto.map(
campaign => this.producerService.getProducerByName(campaign.producer_name).pipe(
map(producer => ({ ...campaign, producer })),
),
))),
).subscribe(campaignListDtoExtended => {
this.campaigns = campaignListDtoExtended;
this.loading = false;
this.fetchProducerMinPrices();
this.fetchProducerTotalCampaigns();
});
}

Related

How to do onething after another in rxjs

I find myself wanting to do this, which feels like it ought to be wrong.
this.isLoading = true;
this.service.getFirstValue().subscribe((response: firstValueType) => {
this.firstValue = response;
this.service.getSecondValue(this.firstValue).subscribe((response: secondValueType) => {
this.secondValue = response;
this.isLoading = false
});
});
What are you supposed to do?
you can use switchMap. Also embedded subscribe is bad practice, supposed to be avoided
this.isLoading = true;
this.service
.getFirstValue()
.pipe(
switchMap(response => {
this.firstValue = response;
return this.service.getSecondValue(this.firstValue);
})
)
.subscribe(response => {
this.secondValue = response;
this.isLoading = false;
});
PS fix the code, subscribe should be outside of pipe
The solution I figured out works fine for me in my project was using the following parts of rxjs:
ConnectableObservable
switchMap
tap
const { Subject } = rxjs;
const { publishReplay, tap, switchMapTo } = rxjs.operators;
// Simulates your service.getFirstValue, ...
const source1$$ = new Subject();
const source2$$ = new Subject();
const source3$$ = new Subject();
// Publish and Replay the last value to be able to connect (make it hot) and always get the lates value
const source1$ = source1$$.pipe(publishReplay(1));
const source2$ = source2$$.pipe(publishReplay(1));
const source3$ = source3$$.pipe(publishReplay(1));
// Connect to make the observable hot
source1$.connect()
source2$.connect()
source3$.connect()
source1$.pipe(
tap(v => console.log('tap source1$: ', v)),
switchMapTo(source2$),
tap(v => console.log('tap source2$: ', v)),
switchMapTo(source3$),
tap(v => console.log('tap source3$: ', v))
).subscribe()
source3$$.next('value 1');
source2$$.next('value 2');
source1$$.next('value 3');
source2$$.next('value 4');
source2$$.next('value 5');
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>
I am not sure if this fits your exactly requirements. But imo this is a at least close szenario like yours. If you want give me feedback and I try to adapt :)
Nested subscribe should be avoided.
In this case I would go like this
this.isLoading = true;
this.service.getFirstValue().pipe(
// this tap is necessary only if you use this.firstValue somewhere else in the code
// otherwise you can skip it and go directly to concatMap
tap((response_1: firstValueType) => this.firstValue = response_1),
// concatMap is usually the preferred operator to use when concatenating http calls
// read the article I have linked below
concatMap(response_1 => this.service.getSecondValue(response_1)),
tap(response_2 => {
this.secondValue = response_2;
this.isLoading = false;
})
).subscribe()
You can get some more inspiration on how to deal with http calls and rxJs reading this article.

Combining observable outputs in to one array

I need to make two calls to Firebase (as it doesn't support OR queries) and merge the output into one array at the end to return to the calling service.
I have something that gets pretty close but it outputs a 2D array of arrays (one for each call to Firebase). I've tried a few things and this is the best I can get to. Any help on tidying up the below would be great.
getAllFriends(): Observable<[Friendship[], Friendship[]]> {
const invitesSent = from(this.afAuth.currentUser.then(user => {
return user.uid;
}))
.pipe(
switchMap(
userid => {
return this.db.collection('friendships', ref => ref.where('inviter', '==', userid)).snapshotChanges().pipe(map(actions => {
return actions.map(action => {
const data = new Friendship(action.payload.doc.data());
data.id = action.payload.doc.id;
console.log(data);
return data;
});
}));
}
)
);
const invitesReceived = from(this.afAuth.currentUser.then(user => {
return user.uid;
}))
.pipe(
switchMap(
userid => {
return this.db.collection('friendships', ref => ref.where('invitee', '==', userid)).snapshotChanges().pipe(map(actions => {
return actions.map(action => {
const data = new Friendship(action.payload.doc.data());
data.id = action.payload.doc.id;
console.log(data);
return data;
});
}));
}
)
);
return combineLatest([invitesSent, invitesReceived]);
}
Friendship is just an object with property: value pairs, nothing special.
I have tried then putting a .pipe() after this returned observable but that just stops the subscription firing in the calling service.
What about returning, at the end, something like this
return combineLatest([invitesSent, invitesReceived]).pipe(
map(([frienships_1, friendships_2]) => ([...friedships_1, ...friendships_2]))
)

Sending multiple actions from Effect, on server response, using action.payload in all of them

What currently works with one action:
#Effect()
addAssignment$ = this.actions$.pipe(
ofType(assignmentActions.AssignmentsActionTypes.AddAssignment),
exhaustMap((action) => {
return this.assignmentDataService.addOrUpdateAssignment([action.payload]).pipe(
map((assignment) => {
return new assignmentActions.AddAssignmentSuccess(assignment);
})
);
}));
How I'm trying to refactor this:
#Effect()
updateAssignment$ = this.actions$.pipe(
ofType(assignmentActions.AssignmentsActionTypes.UpdateAssignment),
map((action) => {
return action.payload;
}),
switchMap((payload) => {
return this.assignmentDataService.addOrUpdateAssignment([payload.postData]);
}),
switchMap((res) => {
return [
new assignmentActions.LastUpdatedAssignmentPost(action.payload.postData),
new assignmentActions.LastUpdatedAssignment(action.payload.mergedData),
new assignmentActions.UpdateAssignmentSuccess(action.payload.mergedData),
];
})
);
How ever ofcourse action.payload.mergedData & action.payload.postData are not available in the last switchMap, and since im quite noob to Effects and Observables I'm breaking my head on this.
Whats the right combination of operators in this one?
To get access to the payload, use the last switchMap in the observable pipeline of this.assignmentDataService.addOrUpdateAssignment API returned observable like this:
#Effect()
updateAssignment$ = this.actions$.pipe(
ofType(assignmentActions.AssignmentsActionTypes.UpdateAssignment),
map((action) => {
return action.payload;
}),
switchMap((payload) => {
return this.assignmentDataService.addOrUpdateAssignment([payload.postData])
.pipe(
switchMap((res) => {
return [
new assignmentActions.LastUpdatedAssignmentPost(payload.postData),
new assignmentActions.LastUpdatedAssignment(payload.mergedData),
new assignmentActions.UpdateAssignmentSuccess(payload.mergedData),
];
})
);
})
);
Hope it helps.
Evantually I solved this just by adding more effects to the equation.
- First effect sends server request... on response sends a UpdateAssignmentSuccess action
- Second effect listens to UpdateAssignmentSuccess and send a LastUpdatedAssignmentPost action
- Third effect listens to LastUpdatedAssignmentPost action and sends a LastUpdatedAssignment action
#Effect()
updateAssignment$ = this.actions$.pipe(
ofType(assignmentActions.AssignmentsActionTypes.UpdateAssignment),
exhaustMap((action) => {
return this.assignmentDataService.addOrUpdateAssignment([action.payload.postData]).pipe(
map((assignment) => {
return new assignmentActions.UpdateAssignmentSuccess(action.payload);
})
);
}),
catchError(() => {
return of({
type: assignmentActions.AssignmentsActionTypes.UpdateAssignmentFailure,
payload: true
});
})
);
#Effect()
updateAssignmentsSuccess$ = this.actions$.pipe(
ofType(assignmentActions.AssignmentsActionTypes.UpdateAssignmentSuccess),
map((action:any) => {
return new assignmentActions.LastUpdatedAssignmentPost(action.payload);
})
);
#Effect()
lastUpdateAssignmentsPost$ = this.actions$.pipe(
ofType(assignmentActions.AssignmentsActionTypes.LastUpdatedAssignmentPost),
map((action:any) => {
return new assignmentActions.LastUpdatedAssignment(action.payload);
})
);

Store dispatch recalls the http get of the effect several times

During dispatch, my effect is called repeatedly until my backend responds and the data is loaded. I need help in understanding how to load the data with just one GET REQUEST and then load from the store if the data is actually already present.
this.cases$ = this.store
.pipe(
takeWhileAlive(this),
select(selectImportTaskCasesData),
tap(
(cases) => {
if (cases.length <= 0) {
this.store.dispatch(new ImportTaskLoadCasesAction());
}
}),
filter((cases) => {
return cases.length > 0;
}),
tap(() => {
this.store.dispatch(new ImportTaskLoadCasesLoadedFromStoreAction());
}),
shareReplay()
);
export const selectCasesData = createSelector(
selectImportTaskCasesState,
state => state ? state.cases : []
);
export const selectImportTaskCasesData = createSelector(
selectCasesData,
cases => {
return cases.slice(0);
}
);
#Effect()
ImportCasesLoad$: Observable<any> = this.actions$
.pipe(
ofType<ImportTaskLoadCasesAction>(ImportCasesActionTypes.ImportTaskLoadCasesAction),
map((action: ImportTaskLoadCasesAction) => action),
switchMap((payload) => {
return this.importCases.get()
.pipe(
map(response => {
return new ImportTaskLoadCasesSuccessAction({ total: response['count'], cases: response['results'] });
}),
catchError((error) => {
this.logger.error(error);
return of(new ImportTaskLoadCasesLoadErrorAction(error));
})
);
})
);
Yes i have a reducer for handeling my Success Action like this :
case ImportCasesActionTypes.ImportTaskLoadCasesSuccessAction:
return {
...state,
loading: false,
cases: action.payload.cases,
total: action.payload.total
};
It's called in my effects.
Does the below work? This is assuming you have a reducer that handles the ImportTaskLoadCasesSuccessAction; Maybe supplying a working example will help, as there is a bit of guessing as how state is being managed.
this.cases$ = this.store
.pipe(
takeWhileAlive(this),
select(selectImportTaskCasesData),
tap(
(cases) => {
if (cases.length <= 0) {
this.store.dispatch(new ImportTaskLoadCasesAction());
}
}),
// personally, I would have the component/obj that is consuming this.cases$ null check the cases$, removed for brevity
shareReplay()
);
export const selectCasesData = createSelector(
selectImportTaskCasesState,
state => state ? state.cases : []
);
export const selectImportTaskCasesData = createSelector(
selectCasesData,
cases => {
return cases.slice(0);
}
);
#Effect()
ImportCasesLoad$: Observable<any> = this.actions$
.pipe(
ofType<ImportTaskLoadCasesAction>(ImportCasesActionTypes.ImportTaskLoadCasesAction),
mergeMap(() => this.importCases.get()
.pipe(
map(response => {
return new ImportTaskLoadCasesSuccessAction({
total: response['count'],
cases: response['results']
});
}),
// catch error code removed for brevity
);
)
);
If you only want the call this.importCases.get() to fire one time, I suggest moving the action dispatch out of the .pipe(tap(...)). As this will fire every time a subscription happens.
Instead, set up this.cases$ to always return the result of select(selectImportTaskCasesData),. Functionally, you probably want it to always return an array. But that is up to your designed desire.
Foe example ...
this.cases$ = this.store
.pipe(
takeWhileAlive(this),
select(selectImportTaskCasesData),
);
Separately, like in a constructor, you can dispatch the this.store.dispatch(new ImportTaskLoadCasesAction());. If you want it to only get called when cases$ is empty, you can always wrap it in a method.
e.g.
export class exampleService() {
ensureCases(): void {
this.store.pipe(
select(selectImportTaskCasesData),
take(1)
).subscribe(_cases => {
if (_cases && _cases.length < 1 ) {
this.store.dispatch(new ImportTaskLoadCasesAction());
}
}),
}
}

how to access previous mergeMap values from rxjs

I am learning to use RXJS. In this scenario, I am chaining a few async requests using rxjs. At the last mergeMap, I'd like to have access to the first mergeMap's params. I have explored the option using Global or withLatest, but neither options seem to be the right fit here.
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => {
return readCSVFile(gauge.id);
}),
mergeMap((csvStr: any) => readStringToArray(csvStr.data)),
map((array: string[][]) => transposeArray(array)),
mergeMap((array: number[][]) => forkJoin(uploadToDB(array, gauge.id))),
catchError(error => of(`Bad Promise: ${error}`))
);
readCSVFile is an async request which returns an observable to read CSV from a remote server.
readStringToArray is another async request which returns an observable to convert string to Arrays
transposeArray just does the transpose
uploadToDB is async DB request, which needs gague.id from the first mergeMap.
How do I get that? It would be great to take some advice on why the way I am doing it is bad.
For now, I am just passing the ID layer by layer, but it doesn't feel to be correct.
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => readCSVFile(gauge.id)),
mergeMap(({ data, gaugeId }: any) => readStringToArray(data, gaugeId)),
map(({ data, gaugeId }) => transposeArray(data, gaugeId)),
mergeMap(({ data, gaugeId }) => uploadToDB(data, gaugeId)),
catchError(error => of(`Bad Promise: ${error}`))
);
Why don't you do simply this?
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => readCSVFile(gauge.id).pipe(
mergeMap((csvStr: any) => readStringToArray(csvStr.data)),
map((array: string[][]) => transposeArray(array)),
mergeMap((array: number[][]) => forkJoin(uploadToDB(array, gauge.id)))
)),
catchError(error => of(`Bad Promise: ${error}`))
);
You can also wrap the inner observable in a function:
uploadCSVFilesFromGaugeID(gaugeID): Observable<void> {
return readCSVFile(gaugeID).pipe(
mergeMap((csvStr: any) => readStringToArray(csvStr.data)),
map((array: string[][]) => transposeArray(array)),
mergeMap((array: number[][]) => forkJoin(uploadToDB(array, gaugeID))
);
}
In order to do this at the end:
const arraySrc$ = from(gauges).pipe(
mergeMap(gauge => uploadCSVFileFromGaugeID(gauge.id)),
catchError(error => of(`Bad Promise: ${error}`))
);
MergeMap requires all observable inputs; else, previous values may be returned.
It is a difficult job to concatenate and display the merging response. But here is a straightforward example I made so you can have a better idea. How do we easily perform sophisticated merging.
async playWithBbservable() {
const observable1 = new Observable((subscriber) => {
subscriber.next(this.test1());
});
const observable2 = new Observable((subscriber) => {
subscriber.next(this.test2());
});
const observable3 = new Observable((subscriber) => {
setTimeout(() => {
subscriber.next(this.test3());
subscriber.complete();
}, 1000);
});
console.log('just before subscribe');
let result = observable1.pipe(
mergeMap((val: any) => {
return observable2.pipe(
mergeMap((val2: any) => {
return observable3.pipe(
map((val3: any) => {
console.log(`${val} ${val2} ${val3}`);
})
);
})
);
})
);
result.subscribe({
next(x) {
console.log('got value ' + x);
},
error(err) {
console.error('something wrong occurred: ' + err);
},
complete() {
console.log('done');
},
});
console.log('just after subscribe');
}
test1() {
return 'ABC';
}
test2() {
return 'PQR';
}
test3() {
return 'ZYX';
}

Resources