Fire an action before sending request in redux-observable epic - javascript

I want to fire an action before sending a request to the server. Here is my code:
public fetchUserPrjects(action$: Observable<IProjectAction>, store: Store<IAppState>) {
return action$.pipe(
ofType(ProjectActionType.FETCH_USER_PROJECTS),
mergeMap((action) => {
store.dispatch(this._commonAction.showLoading()); // <== call action before request
return this._projectService.getProjectList(action.payload)
.map((result) => {
return {
project: result
};
});
}),
flatMap(data => [
this._projectAction.addUserProjects(data),
this._commonAction.hideLoading()
])
).catch((error) => {
return Observable.of(this._commonAction.showError(error));
})
.concat(Observable.of(this._commonAction.hideLoading()));
}
I have tried many ways and ended up this way. However, this way sometimes works but sometimes doesn't. Sometimes it freezes whole the process. How can I fire an action before sending the request to the server?

You could remove the showLoading dispatch from your fetchUserProjects epic and then just create a second epic with:
return action$.pipe(
ofType(ProjectActionType.FETCH_USER_PROJECTS),
map(() => this._commonAction.showLoading())
);
The order of the execution does not really matter because the request this._projectService.getProjectList is asynchronous and will therefore definitely resolve afterwards.

How about this:
return action$.pipe(
ofType(ProjectActionType.FETCH_USER_PROJECTS),
flatMap(action => Observable.concat(
this._projectService.getProjectList(action.payload)
.map(this._projectAction.addUserProjects({ project: result}))
.startWith(this._commonAction.showLoading())
.catch(error => this._commonAction.showError(error)),
Observable.of(this._commonAction.hideLoading())
)))

Related

NGRX - dispatch action on catchError effect with different payload

I need to call an API that can return errors, warnings or success.
If it returns warnings the user must able to accept the warning and I should send the same payload + acceptWarning: true.
I need to display an ionic modal and wait for the user's response to see if he accepts or cancel the warning.
What should be the best way to achieve that?
Right now I have something like this:
#Effect()
public Assign$ = this.actions$.pipe(
ofType(myActions.Assign),
map(action => action.payload),
exhaustMap(assignment =>
this.assignService.assign(assignment).pipe(
switchMap(() => {
this.errorService.showPositiveToast(' Assigned Successfully');
return [
new LoadAssignments(),
new LoadOtherData()
];
}),
catchError(error =>
from(this.openErrorModal(error)).pipe(
switchMap(({ data = '' }) => {
if (data === 'Accept') {
return of(new Assign({ ...assignment, acceptWarning: true }));
}
return of(new AssignShipmentFailure());
})
)
)
)
)
);
async openErrorModal(response: any) {
const errorModal = await this.modalCtrl.create({
component: ErrorValidationPopup,
componentProps: {
response: response,
},
});
await errorModal.present();
return errorModal.onDidDismiss();
}
But it is not triggering the Assign action again. Thanks for your help
If any error occurred in the effect's observable (or any Observable), then its stream emitted no value and it immediately errored out. After the error, no completion occurred, and the Effect will stop working.
To keep the Effect working if any error occurred, you have to swichMap instead of exhaustMap, and handle the errors within the inner observable of the switchMap, so the main Observable won't be affected by that.
Why use switchMap?
The main difference between switchMap and other flattening operators is the cancelling effect. On each emission the previous inner observable (the result of the function you supplied) is cancelled and the new observable is subscribed. You can remember this by the phrase switch to a new observable
You can try something like the following:
#Effect()
public Assign$ = this.actions$.pipe(
ofType(myActions.Assign),
map(action => action.payload),
switchMap(assignment =>
this.assignService.assign(assignment).pipe(
switchMap(() => {
this.errorService.showPositiveToast('Assigned Successfully');
return [
new LoadAssignments(),
new LoadOtherData()
];
}),
catchError(error =>
from(this.openErrorModal(error)).pipe(
map(({ data = '' }) => {
if (data === 'Accept') {
return new Assign({ ...assignment, acceptWarning: true });
}
return new AssignShipmentFailure();
})
)
)
)
)
);

Cannot get result of first HTTP request using RxJS' switchMap

I am trying to make 2 HTTP requests and in the first call I try to create a record and then according to its results (response from the API method) I want to execute or omit the second call that updates another data. However, although I can catch the error in catchError block, I cannot get the response in the switchMap method of the first call. So, what is wrong with this implementation according to teh given scenario? And how can I get the response of the first result and continue or not to the second call according to this first response?
let result;
let statusCode;
this.demoService.create(...).pipe(
catchError((err: any) => { ... }),
switchMap(response => {
// I am trying to get the response of first request at here
statusCode = response.statusCode;
if(...){
return this.demoService.update(...).pipe(
catchError((err: any) => { ... }),
map(response => {
return {
result: response
}
}
)
)}
}
))
.subscribe(result => console.log(result));
The question is still vague to me. I'll post a more generic answer to make few things clear
There are multiple things to note
When an observable emits an error notification, the observable is considered closed (unless triggered again) and none of the following operators that depend on next notifications will be triggered. If you wish to catch the error notifications inside the switchMap, you could return a next notification from the catchError. Something like catchError(error => of(error)) using RxJS of function. The notification would then be caught by the following switchMap.
You must return an observable from switchMap regardless of your condition. In this case if you do not wish to return anything when the condition fails, you could return RxJS NEVER. If you however wish to emit a message that could be caught by the subscriptions next callback, you could use RxJS of function. Replace return NEVER with return of('Some message that will be emitted to subscription's next callback');
import { of, NEVER } from 'rxjs';
import { switchMap, catchError, map } from 'rxjs/operators';
this.demoService.create(...).pipe(
catchError((err: any) => { ... }),
switchMap(response => {
statusCode = response.statusCode;
if (someCondition) {
return this.demoService.update(...).pipe( // emit `update()` when `someCondition` passes
catchError((err: any) => { ... }),
map(response => ({ result: response }))
);
}
// Show error message
return NEVER; // never emit when `someCondition` fails
}
)).subscribe({
next: result => console.log(result),
error: error => console.log(error)
});
You can implement with iif
this.demoService
.create(...)
.pipe(
// tap first to be sure there's actually a response to process through
tap(console.log),
// You can use any condition in your iif, "response.user.exists" is just a sample
// If the condition is true, it will run the run the update$ observable
// If not, it will run the default$
// NOTE: All of them must be an observable since you are inside the switchMap
switchMap(response =>
iif(() =>
response.user.exists,
this.demoService.update(response.id), // Pass ID
of('Default Random Message')
)
),
catchError((err: any) => { ... })
);

Call another epic inside one epic RXJS

I'm quite new to RXJS and development in general. I started working with rxjs recently and I found myself stuck with the following issue and I would appreciate some help/ guidance and some explanation please.
export const updateSomethingEpic1 = (action$) =>
action$
.ofType('UPDATE_SOMETHING')
.switchMap(({ result }: { result }) =>
//SOME API CALL
.map(({ response }) => updateSomethingSuccess(response))
**make call to second epic**
.catch(err => updateSomethingError(err)),
);
//My second epic
export const updateSomethingEpic2 = (action$) =>
action$
.ofType('UPDATE_SOMETHING2')
.switchMap(({ result }: { result }) =>
//SOME API CALL
.map(({ response }) => updateSomethingSuccess2(response))
.catch(err => updateSomethingError2(err)),
);
My question is how would I make a call to my second epic after my first epic has called the api and made a successful request. Want to make a call in the first epic after updateSomethingSuccess action, which adds response to the store and then call the second api afterwards.
Just do return an action.
Code from your sample
.map(({ response }) => updateSomethingSuccess(response) **<--- make call to second epic**)
use
.map(({ response }) => updateSomethingSuccess(response) )
where updateSomethingSuccess(response) is action creator like following
function updateSomethingSuccess(response) {
return { type: 'UPDATE_SOMETHING2', payload: response}; // HERE
}
Then your epic 2 with ofType UPDATE_SOMETHING2 will be performed.
It's dead simple. You have to modify for your needs

React-Redux - using a response from a fetch request in componentDidMount in another request in the component

I have a component that fires off a fetch request (in redux) on componentDidMount. In the same component I need to fire off another redux fetch request using the response data from the first one, preferably before the render.
Due to the fetch being executed in the redux action I havent been able to do this with a promise in componentDidMount as the promise resolves when the action starts and not finishes.
on research I thought it might be able to do it with componentWillRecieveProps however I don't fully understand how and also read that this hook is being depreciated soon.
any help would be greatly appreciated.
first action:
componentDidMount(){
this.props.onGetSessionId(this.parseToken(this.props.location.search))
};
secondAction:
this.props.onFetchFavourites(this.props.sessionId)
It's hard to help here without seeing the code for how onGetSessionId and onFetchFavourites are implemented but assuming that you're using redux-thunk, it's probably something like this:
function getSessionId(token) {
return function (dispatch) {
return fetchSessionId(token)
.then(
response => dispatch(getSessionIdSuccess(forPerson, response.id))
)
.catch(
error => dispatch(getSessionIdFailure(error)
)
};
}
function getFavorites(sessionId) {
return function (dispatch) {
return fetchFavorites(sessionId)
.then(
response => dispatch(getFavoritesSuccess(sessionId, response.favorites))
)
.catch(
error => dispatch(getFavoritesFailure(error)
)
};
}
fetch returns a promise so you can just keep chaining .then and using the return value from the previous then. this lets you do something like
function getSessionIdAndFavorites(token) {
return function (dispatch) {
return fetchSessionId()
.then(
response => {
dispatch(getSessionIdSuccess(response.id))
return response.id
}
)
.then(id => {dispatch(getFavorites(id)}
.catch(
error => dispatch(getSessionAndFavoritesIdFailure(error)
)
};
}
In onGetSessionId you can return promise, like this:
function onGetSessionId(param) {
return new Promise((resolve, reject) => {
apiClient.call()
.then(data => {
resolve(data);
})
.catch(error => {
reject(error)
})
})
}
and then in componentDidMount:
componentDidMount(){
this.props.onGetSessionId(this.parseToken(this.props.location.search))
.then(data => {
// ... do stuff with returned data
})
}

How to delay one epic until another has emitted a value

In redux-observable I need to wait before doing an API request until another epic has completed. Using combineLatest doesn't work, nothing happens after APP_INITIALIZED finishes. I also tried withLatestFrom but neither works.
const apiGetRequestEpic = (action$, store) => {
return Observable.combineLatest(
action$.ofType('APP_INITIALIZED'),
action$.ofType('API_GET_REQUEST')
.mergeMap((action) => {
let url = store.getState().apiDomain + action.payload;
return ajax(get(url))
.map(xhr => {
return {type: types.API_RESPONSE, payload: xhr.response};
})
.catch(error => {
return Observable.of({ type: 'API_ERROR', payload: error });
});
})
);
};
combineLatest definition
One approach is (using pseudo names), when receiving the initial action API_GET_REQUEST you immediately start listening for a single INITIALIZE_FULFILLED, which signals that the the initialization (or whatever) has completed--we'll actually kick it off in a bit. When received, we mergeMap (or switchMap whichever for your use case) that into our call to make the other ajax request and do the usual business. Finally, the trick to kick off the actual initialization we're waiting for is adding a startWith() at the end of that entire chain--which will emit and dispatch the action the other epic is waiting for.
const initializeEpic = action$ =>
action$.ofType('INITIALIZE')
.switchMap(() =>
someHowInitialize()
.map(() => ({
type: 'INITIALIZE_FULFILLED'
}))
);
const getRequestEpic = (action$, store) =>
action$.ofType('API_GET_REQUEST')
.switchMap(() =>
action$.ofType('INITIALIZE_FULFILLED')
.take(1) // don't listen forever! IMPORTANT!
.switchMap(() => {
let url = store.getState().apiDomain + action.payload;
return ajax(get(url))
.map(xhr => ({
type: types.API_RESPONSE,
payload: xhr.response
}))
.catch(error => Observable.of({
type: 'API_ERROR',
payload: error
}));
})
.startWith({
type: 'INITIALIZE'
})
);
You didn't mention how everything works, so this is just pseudo code that you'll need to amend for your use case.
All that said, if you don't ever call that initialization except in this location, you could also just include that code directly in the single epic itself, or just make a helper function that abstracts it. Keeping them as separate epics usually means your UI code can independently trigger either of them--but it might still be good to separate them for testing purposes. Only you can make that call.
const getRequestEpic = (action$, store) =>
action$.ofType('API_GET_REQUEST')
.switchMap(() =>
someHowInitialize()
.mergeMap(() => {
let url = store.getState().apiDomain + action.payload;
return ajax(get(url))
.map(xhr => ({
type: types.API_RESPONSE,
payload: xhr.response
}))
.catch(error => Observable.of({
type: 'API_ERROR',
payload: error
}));
})
.startWith({ // dunno if you still need this in your reducers?
type: 'INITIALIZE_FULFILLED'
})
);
I think that you are not using combineLatest the way it is intended. Your example should be written as:
const apiGetRequestEpic = (action$, store) => {
return (
Observable.combineLatest(
action$.ofType('APP_INITIALIZED').take(1),
action$.ofType('API_GET_REQUEST')
)
.map(([_, apiGetRequest]) => apiGetRequest)
.mergeMap((action) => {
let url = store.getState().apiDomain + action.payload;
return ajax(get(url))
.map(xhr => {
return {type: types.API_RESPONSE, payload: xhr.response};
})
.catch(error => {
return Observable.of({ type: 'API_ERROR', payload: error });
});
})
);
};
This way, this combineLatest will emit whenever an 'API_GET_REQUEST' is emitted, provided that 'APP_INITIALIZED' has ever been dispatched at least once, or, if not, wait for it to be dispatched.
Notice the .take(1). Without it, your combined observable would emit anytime 'APP_INITIALIZED' is dispatched as well, most likely not what you want.

Categories