How to delay one epic until another has emitted a value - javascript

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.

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

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

How to make multiple API requests in a Promise in a Redux action

I'm working with React/Redux in a project and I need to make 2 separate API requests, but the second request depends on the first returning without any issues. In the below example of an action, I'm trying to wrap both calls in a promise, but it's not quite working (getting a Uncaught Error: Actions must be plain objects. Use custom middleware for async actions. error in the console). I don't necessarily need to do anything with the response data as a result of both calls. I just need them to return a 200 status or an error.
Note: Unfortunately, I can't use async/await for this example. Thanks!
export default () => {
const url = '/api';
const options = {...}
const otherOptions = {...}
return new Promise((resolve, reject) => {
return dispatch =>
// First API request
axios.post(url, options)
.then(responseA => dispatch({ type: RESPONSE_A_SUCCESS }))
.then(() =>
// Second separate API request
axios.post(url, otherOptions)
.then(responseB => {
dispatch({ type: RESPONSE_B_SUCCESS });
resolve();
})
)
.catch(error => {
dispatch({ type: errorActionType, error: error });
reject(error);
});
});
};
Your code has 2 issues:
It returns a promise, which is not a "plain object".
You are nesting promises instead of attaching them sequentally
Try this instead:
export default () => {
const url = '/api';
const options = {...}
const otherOptions = {...}
return dispatch =>
axios.post(url, options)
.then(responseA => dispatch({ type: RESPONSE_A_SUCCESS }))
.then(() => axios.post(url, otherOptions))
.then(responseB => dispatch({ type: RESPONSE_B_SUCCESS }))
.catch(error => {
dispatch({ type: errorActionType, error: error });
reject(error);
});
});
};

How to make redux-observable send multiple actions before and after ajax request?

I am trying to write Epic for redux-observable. But I am having a problem (
ON_EMPTY_FORM_STATE action is executed before .map(response => formSavedAction(response)) is executed.
How can I fix it? Basically what I am trying to get is:
Capture FORM_SEND action
Send Ajax request with payload
If i get 200 response from server then execute formSavedAction(response)
If i get 500 error then dispatch { type: ON_FORM_SUBMIT_ERROR }
At the end (no matter what Ajax request returned) I want to dispatch action { type: EMPTY_FORM_STATE }
And here is my code:
const saveProductEpic = (action$, $store) =>
action$.ofType(SUBMIT_FORM)
.mergeMap(action => ajax.post('http://localhost:9000/products', action.payload,
{ 'Content-Type': 'application/json' }))
.map(response => formSavedAction(response))
.catch(err => { $store.dispatch({ type: ON_FORM_SUBMIT_ERROR }) })
.do(() => { $store.dispatch({ type: EMPTY_FORM_STATE }) })
The issue is that action { type: EMPTY_FORM_STATE } is dispatched Before
and not after AJAX request.
Is there any way to fix this?
The way redux-observable works is that the observable returned by the epic function is subscribed to in the middleware. Your observable must emit redux actions because the subscribe function is just: action => store.dispatch(action)
Because you are using store.dispatch in your epic directly, you are circumventing redux-observable and dispatching things out of order from what you're expecting.
To emit the actions in the order you are expecting you can do this:
action$.ofType(SUBMIT_FORM)
.mergeMap(action => ajax.post('http://localhost:9000/products', action.payload,
{ 'Content-Type': 'application/json' }))
.map(response => formSavedAction(response))
// Notice that catch is now returning the value by dropping the brackets.
.catch(err => Observable.of({ type: ON_FORM_SUBMIT_ERROR }))
// send whatever action has come through, and add the empty state action after it.
.mergeMap(action => Observable.of(action, { type: EMPTY_FORM_STATE }))
Concept Demo: https://jsbin.com/focujikuse/edit?js,console
const saveProductEpic = (action$, $store) => action$.ofType(SUBMIT_FORM)
.mergeMap((data) => Rx.Observable.concat(
ajax.post(url, data.payload, params).mergeMap(
(response) => Rx.Observable.of(formSavedAction(response)) )
.catch(err => { $store.dispatch({ type: ON_FORM_SUBMIT_ERROR }) }),
Rx.Observable.of({ type: ON_EMPTY_FORM_STATE })
.delay(2000)
));
const saveProductEpic = (action$, $store) =>
action$.ofType(SUBMIT_FORM)
.mergeMap(action => ajax.post('http://localhost:9000/products',action.payload,
{ 'Content-Type': 'application/json' })
.mergeMap(response => Observable.of(formSavedAction(response),
emptyFormStateAction()))
.catch(err => Observable.of(
formSubmitErrorAction(),
emptyFormStateAction())
)
);
where emptyFormStateAction and formSubmitErrorAction are action creators.

Use one output of epic as another input of epic in react observable

I'm using react-observable to develop a react-native application.
I have two epics, one is used to fetch the latitude and longitude, and another is used to get the address through the latitude and longitude.
My question is, as I want to do one thing in one epic, how could I use the first epic's output as the second epic's input?
const getLocationEpic = action$ =>
action$.ofType('GET_LOCATION')
.mergeMap(() =>
currentPosition$()
.map(data => ({
type: 'GET_LOCATION_SUCCESS',
data: {"latitude": data.coords.latitude, "longitude": data.coords.longitude}
}))
.catch(error => {
console.log("GET POSITION ERR", error);
return Observable.of({type: 'GET_POSITION_ERROR', error: error});
})
);
const getAddressEpic = action$ =>
action$.ofType('GET_ADDRESS')
.switchMap(action =>
ajax(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}`)
.map(data => (data))
.do({
next: data => {
console.log("address data");
console.log(data);
}
})
.map(data => ({
type: 'GET_POSITION_SUCCESS', data: data.response.results[0].formatted_address
}))
.mergeMap(() =>
currentPosition$()
.map(data => ({
type: 'GET_ADDRESS_SUCCESS',
data: {"latitude": data.coords.latitude, "longitude": data.coords.longitude}
}))
.catch(error => {
console.log("GET_ADDRESS_ERROR", error);
return Observable.of({type: 'GET_ADDRESS_ERROR', error: error});
})
));
it might be something like this:
getLocationEpic(action)
.mergeMap( (action) => getAddressEpic(action) )
.subscribe( (data) => {
/* get the result from epic 1 to epic 2 */
})
Detail: Take result of getLocationEpic, then flatten it and feed the result into getAddressEpic. Subscribe on it for the final result.
In redux-observable your epics receive a stream of all actions dispatched--what may not be obvious is that includes actions dispatched by other epics. This is a very powerful feature because you can write epics that don't need to be coupled to implementation details in the other epics. The action is the contract.
In your case, you said you want to sequence them as 1. get longitude/latitude 2. then use that long/lat to get their address.
Your getAddressEpic then just needs to listen for GET_LOCATION_SUCCESS to be notified and receive the long/lat values.
const getAddressEpic = action$ =>
action$.ofType('GET_LOCATION_SUCCESS') // <--- listen for location success action
.switchMap(action =>
// you can now use the long/lat from that action
ajax(`${apiURL}/geocode/json?latlng=${action.data.latitude},${action.data.longitude}`)

Categories