What i am trying to do is:
Capture 'PRODUCTS_DISPLAY' action in reducer
Dispatch 'ON_PRODUCTS_FETCHNG' action
Load data via AJAX
If response is OK dispacth 'DONE_PRODUCTS_FETCHING' with payload from AJAX response
In case of AJAX error dispatch 'ON_PRODUCTS_FETCH_ERR'
and here is what i have already "coded":
const fetchProductsEpic = (action$, $store) =>
action$.ofType(PRODUCTS_DISPLAY)
.do($store.dispatch({ type: 'ON_PRODUCTS_FETCHNG' }))
.mergeMap(action => ajax.getJSON(`http://localhost:9000/products`)
.map(response => productsFetchedAction(response))
.catch(err => {
$store.dispatch({ type: 'ON_PRODUCTS_FETCH_ERR', payload: err })
})
);
And i think I am kind of messed up with this code. I don't think that calling
.do($store.dispatch({ type: 'ON_PRODUCTS_FETCHNG' }))
manually is a good practice but i don't see any other way to do this.
Is there any way to code this epic more rationally and with best practices?
Related
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
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())
)))
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.
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}`)
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.