Firing multiple actions on catch error - redux-observable

I am struggling to figure out how to fire multiple actions in a catch error handler in my epics.
I have successfully figured out how to fire multiple actions on a successful async call in my epics using the thunk-middleware. See below:
const editDomainsEpic = (action$) =>
action$
.ofType(EDIT_DOMAINS)
.mergeMap((action) =>
Rx.Observable.fromPromise(api.editDomains(action.payload))
// Here we are using Redux thunk middleware to execute
// a function instead of just dispatching an action
// so that we can disptach two actions
// ----------------- vvv
.map((domain) => (dispatch) => {
// Display Growl Notifications
dispatch(
displayGrowlNotification(
MESSAGE_TYPES.SUCCESS,
`${domain.name} was saved`
)
)
// Fire Success Action
dispatch({
type: EDIT_DOMAINS_SUCCESS,
payload: { domain }
})
})
.catch((error) => Rx.Observable.of({
type: EDIT_DOMAINS_ERROR,
payload: { error }
}))
.takeUntil(action$.ofType(EDIT_DOMAINS_CANCEL))
)
Can anyone guide me to how I can have the catch return or fire two observable actions that will get dispatched similarly to how I did with the success?

Observable.of() supports an arbitrary number of arguments and will emit them all sequentially one after the other, so to emit more than one action in your catch, you just add more arguments.
With that knowledge at hand, you can also use it to dispatch multiple actions for success instead of emitting a thunk and imperatively calling dispatch yourself.
const editDomainsEpic = (action$) =>
action$
.ofType(EDIT_DOMAINS)
.mergeMap((action) =>
Rx.Observable.fromPromise(api.editDomains(action.payload))
.mergeMap((domain) => Rx.Observable.of(
displayGrowlNotification(
MESSAGE_TYPES.SUCCESS,
`${domain.name} was saved`
), {
type: EDIT_DOMAINS_SUCCESS,
payload: { domain }
}
))
.catch((error) => Rx.Observable.of({
type: EDIT_DOMAINS_ERROR,
payload: { error }
}, {
type: ANOTHER_ONE,
payload: 'something-else'
}))
.takeUntil(action$.ofType(EDIT_DOMAINS_CANCEL))
)
This would be more idiomatic RxJS (and thus redux-observable) but it's not necessarily a requirement.

Related

How to dispatch two actions in one epic, which could be in the same or in another reducer

I have two ducks (ui and backend) with epics in them.
I need to trigger two actions after finishing backend operations
One of these actions reside in the backend duck, the other in the ui duck.
I started with the background action and things worked as expected.
Adding the second action leads me to issues, as I can reach the action (console logs correctly), but not the reducer (no log)
The challenge I'm trying to solve is:
Kicking off two actions in one epic
dispatching an action in another reducer
My code looks similar to this:
the backendDuck's epic:
fetchFooEpic : (action$, store) =>
action$.pipe(
operators.filter(action => action.type === types.LOAD),
// start added section for second call
operators.switchMap(action => {
const response = operators.from(fetchSomeUrl(action))
.pipe(
operators.of(uiDuck.actions.fetchUserFulfilled(response.props)),
),
operators.catchError(err => {
console.error('Error happened!', err.message)
return rxjs.of({ type: types.ADD_CATEGORY_ERROR, payload: err })
})
return response
}),
// start added section for second call
// original first call
operators.map(a => ({ type: types.ENDACTION, payload: a.payload })),
operators.catchError(err => {
console.error('Error happened!', err.message)
return rxjs.of({ type: types.ADD_CATEGORY_ERROR, payload: err })
})
)
the uiDuck:
export actions={
...
fetchUserFulfilled: (value) => {
console.log('hello from action')
return ({ type: types.FETCHUSERFULFILLED, payload: value })
},
...
}
...
export default function reducer(state = initialState, action) {
switch (action.type) {
case types.FETCHUSERFULFILLED:
console.log('hello from reducer')
return {
...state,
user: action.payload,
}
...
Turns out I was combining the two calls in the wrong way.
For being able to pipe along, the piped observable needs to return an observable again.
When mapping to another redux-action, it seems to me that it doesn't return an observable (?) thus, the call needs to happen for all desired redux-actions at the same location (eg with concat)
For the sake of completeness I strive to explain all parts of the code in comments
import * as operators from 'rxjs'
fetchFooEpic : (action$, store) =>
action$.pipe(
operators.filter(action => action.type === types.LOAD), // Filter
operators.switchMap(action => // restart inner actions on each call
operators.from(fetchSomeUrl(action)) // creating observable from result
.pipe( // starting new flow on observable (self)
//operators.tap(a => console.log('Now running fetchfooepic 2', a)), // dedicated location for sideeffects
operators.switchMap( // restart inner actions on each call
(response) => operators.concat( // Kicking off several actions sequentially (merge() would do that in parallel)
operators.of(uiDuck.actions.fetchUserFulfilled(response)), // addressing the redux action in other reducer
operators.of(({ // addressing the redux action via the type in this duck (ENDACTION is controlled by epics only, no action exists for it)
type: types.ENDACTION,
payload: response
}})),
)),
operators.catchError(err =>{
console.error('Shit happens!', err.message) // errorhandling
return rxjs.of({ type: types.ADD_CATEGORY_ERROR, payload: err })
})
)
)
),
Generally the functions are documented with some (more or less understandable) examples in
https://rxjs.dev/api/index/function/

how to pass a reference to a component when calling a vuex action

I'm fairly new to vue (and very new to vuex). I would like to move some axios api calls to be actions in my Vuex store. I know have for example:
actions:{
LOAD_USER: function ({ commit }) {
axios.get('/arc/api/v1/me', {dataType: 'json'})
.then((response )=> {
commit('SET_USER', { user: response.data.user })
})
.catch(function (error) {
console.log(error.message);
});
and call this in my calling component via:
this.$store.dispatch('LOAD_USER')
and this is working. My problem is that I need to set some variables in the calling component to false or kill a progress bar. Here's what I was previously using in my calling component:
this.loading = true
this.$Progress.start()
axios.get('/arc/api/v1/me', {dataType: 'json'})
.then((response )=> {
this.$Progress.finish()
this.loading = false
this.$store.state.user = response.data.user;
this.user = this.$store.state.user
})
.catch(function (error) {
this.$Progress.fail()
console.log(error.message);
});
How would I integrate these loading behaviors into my vuex action? How would I pass a reference to my component via this call:
this.$store.dispatch('LOAD_USER')
or is there a better solution?
Well, you can always use the second parameter of Store.dispatch() to pass any payload into the corresponding action:
this.$store.dispatch('LOAD_USER', this); // passing reference as payload
... but I strongly recommend against doing this. Instead, I'd rather have the whole state (including 'loading' flag, etc.) processed by VueX.
In this case, a single action - LOAD_USER, based on asynchronous API request - would commit two mutations to Store: the first one sets loading flag when the request has been started, the second one resets it back to false - and loads the user data. For example:
LOAD_USER: function ({ commit }) {
commit('LOADING_STARTED'); // sets loading to true
axios.get('/arc/api/v1/me', {dataType: 'json'})
.then(response => {
commit('LOADING_COMPLETE'); // resets loading flag
commit('SET_USER', { user: response.data.user });
})
.catch(error => {
commit('LOADING_ERROR', { error }); // resets loading
console.log(error.message);
});
This approach, among the other advantages, simplifies things a lot when your requests' logic gets more complicated - with error handling, retries etc.
Actions can return a promise https://vuex.vuejs.org/en/actions.html
I think what you want to do is activate the loading when you call your action and stop the loading when the promise is resolved or rejected.
// Action which returns a promise.
actions: {
LOAD_USER ({ commit }) {
return new Promise((resolve, reject) => {
axios.get('/arc/api/v1/me', {dataType: 'json'})
.then((response )=> {
commit('SET_USER', { user: response.data.user })
resolve()
})
.catch(function (error) {
console.log(error.message);
reject(error);
});
})
}
}
// Update loading when the action is resolved.
this.loading = true;
store.dispatch('LOAD_USER').then(() => {
this.loading = false;
})
.catch(function(error) {
// When the promise is rejected
console.log(error);
this.loading = false;
});
If you can't achieve your goal using the above you can add the loading boolean to your vuex store and import it in your component. Than modify the loading boolean inside your action (using mutations) to let the view update.
Note: I would not pass a reference to your actions. While this is possible there are likely better solutions to solve your problem. try to keep the view logic in your components whenever possible.

Using redux-observable and subscribing to a websocket

Trying to figure out how to get my epic going which will subscribe to a websocket and then dispatch some actions as the emitted events roll in from the websocket.
The sample I see are using a multiplex and not actually calling a subscribe on websocket, and I'm confused a bit on changing it up.
I have started it like this. But I believe the redux observable is wanting an
const socket$ = Observable.webSocket<DataEvent>(
"ws://thewebsocketurl"
);
const bankStreamEpic = (action$, store) =>
action$.ofType(START_BANK_STREAM).mergeMap(action => {
console.log("in epic mergeMap");
socket$
.subscribe(
e => {
console.log("dispatch event " + e);
distributeEvent(e);
},
e => {
logger.log("AmbassadorsDataService", "Unclean socket closure");
},
() => {
logger.log("AmbassadorsDataService", "Socket connection closed");
}
)
});
function distributeEvent(event: DataEvent) : void {
//this.logger.log('AmbassadorsDataService', 'Event Received: ' + event.command + ' and id: ' + event.id);
if(event.source === '/ambassadors/bank') {
if( event.command === 'REMOVE') {
removeDataEvent(event);
}
else if(event.command == 'ADD') {
loadDataEvent(event);
}
}
}
It is throwing an error:
Uncaught TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
Any help would be appreciated!
Thanks
In redux-observable, you will almost never (unless you know why I say "almost") call subscribe yourself. Instead, Observables are chained and the middleware and other operators will handle subscriptions for you.
If all you want to do is dispatch an action for every event received, that's simple:
const socket$ = Observable.webSocket<DataEvent>(
"ws://thewebsocketurl"
);
const bankStreamEpic = (action$, store) =>
action$.ofType('START_BANK_STREAM')
.mergeMap(action =>
socket$
.map(payload => ({
type: 'BANK_STREAM_MESSAGE',
payload
}))
);
You may (or may not) need to do more customization depending on what the content of the message received from the socket is, but actually you might be better served placing that other logic in your reducers since it probably isn't side effect related.
You probably will want a way to stop the stream, which is just a takeUntil:
const socket$ = Observable.webSocket<DataEvent>(
"ws://thewebsocketurl"
);
const bankStreamEpic = (action$, store) =>
action$.ofType('START_BANK_STREAM')
.mergeMap(action =>
socket$
.map(payload => ({
type: 'BANK_STREAM_MESSAGE',
payload
}))
.takeUntil(
action$.ofType('STOP_BANK_STREAM')
)
);
I used mergeMap because you did, but in this case I think switchMap is more apt, since each having multiple of these seems redundant, unless you need to have multiple and your question just omits something unique about each.

redux-observable to get current location

I'm trying to use react native Geolocation to getCurrentPosition and then as soon as the position is returned, use react native geocoder to use that position to get the location. I'm using redux-observable epics to get all of this done.
Here are my two epics:
location.epic.js
import { updateRegion } from '../map/map.action'
import Geocoder from 'react-native-geocoder'
export const getCurrentLocationEpic = action$ =>
action$.ofType(GET_CURRENT_LOCATION)
.mergeMap(() =>
Observable.fromPromise(Geocoder.geocodePosition(makeSelectLocation()))
.flatMap((response) => Observable.of(
getCurrentLocationFulfilled(response)
))
.catch(error => Observable.of(getCurrentLocationRejected(error)))
)
export const getCurrentPositionEpic = action$ =>
action$.ofType(GET_CURRENT_POSITION)
.mergeMap(() =>
navigator.geolocation.getCurrentPosition(
(position) => Observable.of(
updateRegion(position),
getCurrentLocation(position)
),
error => Observable.of(getCurrentPositionRejected(error)),
{ enableHighAccuracy: true, timeout: 20000, maximumAge: 1000 }
).do(x => console.log(x))
).do(x => console.log(x))
As soon as the app starts, this code executes:
class Vepo extends Component {
componentDidMount() {
const { store } = this.context
this.unsubscribe = store.subscribe(() => { })
store.dispatch(fetchCategories())
store.dispatch(getCurrentPosition())
}
fetchCategories() is an action that has an epic too, but that is working. dispatching the getCurrentPosition() action runs through the epic above. The only output that I can see is that my reducer handles getLocationRejected() as it console logs this:
there was an issue getting your current location: Error: invalid position: {lat, lng} required
at Object.geocodePosition (geocoder.js:15)
at MergeMapSubscriber.project (location.epic.js:17)
at MergeMapSubscriber._tryNext (mergeMap.js:120)
at MergeMapSubscriber._next (mergeMap.js:110)
at MergeMapSubscriber.Subscriber.next (Subscriber.js:89)
at FilterSubscriber._next (filter.js:88)
at FilterSubscriber.Subscriber.next (Subscriber.js:89)
at Subject.next (Subject.js:55)
at Object.dispatch (createEpicMiddleware.js:72)
at Object.dispatch (devTools.js:313)
Here is my reducer:
const searchPage = (
initialLocationState = initialState.get('searchForm').get('location'),
action: Object): string => {
switch (action.type) {
case GET_CURRENT_LOCATION_FULFILLED: {
return action.payload
}
case GET_CURRENT_LOCATION_REJECTED: {
console.log('there was an issue getting your current location: ',
action.payload)
return initialLocationState
}
case GET_CURRENT_POSITION_REJECTED: {
console.log('there was an issue getting your current position: ',
action.payload)
return initialLocationState
}
default:
return initialLocationState
}
}
Is there anything obvious I am doing wrong? My attempt to debug by adding .do(x => console.log(x)) does nothing, nothing is logged to the console. updateRegion() never does fire off because that dispatches an action and the reducer UPDATE_REGION never executes. But the execution must make it into the success case of getCurrentPosition() eg:
(position) => Observable.of(
updateRegion(position),
getCurrentLocation(position)
),
must execute because the getCurrentLocation(position) does get dispatched.
Where am I going wrong?
What would be your technique for using an epic on a function which takes a callback function? getCurrentPosition() takes a callback and the callback handles the payload. Basically if you remove Observable.of( from inside getCurrentPosition(), that's how getCurrentPosition() is correctly used - and has been working for me without redux-observable.
Wrapping anything in a custom Observable is fairly simple, very similar to creating a Promise except Observables are lazy--this is important to understand! RxJS Docs
In the case of geolocation, there are two main APIs, getCurrentPosition and watchPosition. They have identical semantics except that watchPosition will call your success callback every time the location changes, not just a single time. Let's use that one since it's natural to model it as a stream/Observable and most flexible.
function geolocationObservable(options) {
return new Observable(observer => {
// This function is called when someone subscribes.
const id = navigator.geolocation.watchPosition(
(position) => {
observer.next(position);
},
error => {
observer.error(error);
},
options
);
// Our teardown function. Will be called if they unsubscribe
return () => {
navigator.geolocation.clearWatch(id);
};
});
}
geolocationObservable({ enableHighAccuracy: true, timeout: 20000, maximumAge: 1000 })
.subscribe(
position => console.log(position),
e => console.error(e)
);
// will log every time your location changes, until you unsubscribe
Since it's now an Observable, if you only want the current location you can just do .take(1).
So using it inside your epic might be like this
// If you want, you could also use .share() to share a single
// underlying `watchPosition` subscription aka multicast, but
// that's outside the scope of the question so I don't include it
const currentPosition$ = geolocationObservable({
enableHighAccuracy: true,
timeout: 20000,
maximumAge: 1000
});
export const getCurrentPositionEpic = action$ =>
action$.ofType(GET_CURRENT_POSITION)
.mergeMap(() =>
currentPosition$
.take(1) // <----------------------------- only the current position
.mergeMap(position => Observable.of(
updateRegion(position),
getCurrentLocation(position)
))
.catch(error => Observable.of(
getCurrentPositionRejected(error)
))
);
As a side note, you might not need to dispatch both updateRegion() and getCurrentLocation(). Could your reducers just listen for a single action instead, since they both seem to be signalling the same intent?

Returning Promise from action creator in React Native using redux-thunk

I have an action creator that is called from my React component:
// ...
import { connect } from 'react-redux';
// ...
import { submitProfile } from '../actions/index';
// ...
onSubmit() {
const profile = {
name: this.state.name
// ...
};
this.props.submitProfile(profile)
.then(() => { // I keep getting an error here saying cannot read property 'then' of undefined...
console.log("Profile submitted. Redirecting to another scene.");
this.props.navigator.push({ ... });
});
}
export default connect(mapStateToProps, { submitProfile })(MyComponent);
The definition of the action creator is something like the following. Note I am using the redux-thunk middleware.
export function submitProfile(profile) {
return dispatch => {
axios.post(`some_url`, profile)
.then(response => {
console.log("Profile submission request was successful!");
dispatch({ ... }); // dispatch some action
// this doesn't seem to do anything . . .
return Promise.resolve();
})
.catch(error => {
console.log(error.response.data.error);
});
};
}
What I want to be able to do is call the action creator to submit the profile and then after that request was successful, push a new route into the navigator from my component. I just want to be able to determine that the post request was successful so I can push the route; otherwise, I would not push anything, but say an error occurred, try again.
I looked up online and found Promise.resolve(), but it doesn't not seem to solve my problem. I know that I could just do a .then after calling an action creator if I was using the redux-promise middleware. How do I do it with redux-thunk?
The return value from the function defined as the thunk will be returned. So the axios request must be returned from the thunk in order for things to work out properly.
export function submitProfile(profile) {
return dispatch => {
return axios.post(`some_url`, profile) // don't forget the return here
.then(response => {
console.log("Profile submission request was successful!");
dispatch({ ... }); // dispatch some action
return Promise.resolve();
})
.catch(error => {
console.log(error.response.data.error);
});
};
}