Using redux-observable and subscribing to a websocket - redux-observable

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.

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/

redux-observable, How to do an operator like promise.all()?

I have two async requests, want to write a epic do the job like promise.all()
const fetchData1 = () => (action$: ActionsObservable<any>, store: any) => (
ajax.getJSON('../../mockData/promiseAll/data1.json').map((data: any) => {
return requestData1Success(data);
})
);
const fetchData2 = () => (action$: ActionsObservable<any>, store: any) => (
ajax.getJSON('../../mockData/promiseAll/data2.json').map((data: any) => {
return requestData2Success(data);
})
)
const requestAllDataEpic = (action$: ActionsObservable<any>, store: any) => {
return action$.ofType(t.REQUEST_ALL_DATA)
.map((action) => action.payload)
.switchMap((names: string[]) => {
console.log(names);
return Observable.forkJoin([
fetchData1()(action$, store),
fetchData2()(action$, store)
])
.map((results: any[]) => {
const [action1, action2] = results;
requestData1Success(action1);
requestData2Success(action2);
});
});
};
But when I dispatch the action, the console give me an error:
Uncaught TypeError: Cannot read property 'type' of undefined
I think the reason is I do not give the middleware an action object, but undefined.
How can I do this correctly?
In the provided example, you are not actually returning your two actions, you're returning nothing:
.map((results: any[]) => {
const [action1, action2] = results;
// v----- not returning either of these
requestData1Success(action1);
requestData2Success(action2);
});
map can't used to emit two actions sequentially because it's 1:1 not 1:many (mergeMap, switchMap, concatMap, etc are 1:many). However, in your example you are already converting the responses to the actions inside your fetchData helpers--doing it again would wrap an action inside another action, not what you want. This looks like a bug when you were refactoring.
Other than that, it's actually not clear what you intended to do. If you have further questions you'll need to describe what you want you'd like to achieve.

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?

Redux Observables: Custom observer with multiple events?

Hi I'm trying to use redux-observables with react native and a websocket wrapper called Phoenix, which basically allows you to execute callbacks when certain messages are received through the websocket.
Here is the basic setup of what I'm trying to do:
import 'rxjs';
import { Observable } from 'rxjs';
import { Socket, Channel } from 'phoenix';
import * as channelActions from '../ducks/channel';
export default function connectSocket(action$, store) {
return action$.ofType(channelActions.SETUP)
.mergeMap(action => {
return new Observable(observer => {
const socket = new Socket('http://localhost:4000/socket');
const userId = store.getState().user.id;
const channel = socket.channel(`user:${userId}`);
channel
.join()
.receive('ok', response => observer.next({ type: 'join', payload: 'something' }))
.receive('error', reason => observer.next({ type: 'error', payload: 'reason' }))
channel.on('rooms:add', room => observer.next({ type: 'rooms:add', payload: '123' }))
channel.on('something:else', room => observer.next({ type: 'something:else', payload: '123' }))
});
})
.map(action => {
switch (action.type) {
case 'join':
return channelActions.join(action.payload);
case 'error':
return channelActions.error(action.payload);
case 'rooms:add':
return channelActions.add(action.payload);
case 'something:else':
return channelActions.something(action.payload);
}
});
};
As you can see, there are several concerns / issues with this approach:
Different events are being fired from one observer. I don't know how to split them up besides that switch statement in the .map() function
I don't know where to call observer.complete() since I want it to continue to listen for all those events, not just once.
If I could extract the channel constant into a separate file available to several epics that would fix these concerns. However, channel is dependent on socket, which is dependent on user, which comes from the redux state.
So I'm quite confused on how to approach this problem. I think the ability to extract a global channel object would fix it, but that also depends on the user ID from the redux state. Any help is appreciated.
P.S. If it's worth anything, my use case is very similar to this guy (https://github.com/MichalZalecki/connect-rxjs-to-react/issues/1). One of the responders recommended using Rx.Subject but I don't know where to start with that...
It seems like you are making your life extra difficult. You have already mapped the events so why are you combining and remapping them? Map each event into its own stream and merge the results together.
// A little helper function to map your nonstandard receive
// function into its own stream
const receivePattern = (channel, signal, selector) =>
Observable.fromEventPattern(
h => channel.receive(signal, h),
h => {/*However you unsubscribe*/},
selector
)
export default function connectSocket(action$, store) {
return action$.ofType(channelActions.SETUP)
// Observable.create is usually a crutch pattern
// Use defer or using here instead as it is more semantic and handles
// most of the stream lifecycle for you
.mergeMap(action => Observable.defer(() => {
const socket = new Socket('http://localhost:4000/socket');
const userId = store.getState().user.id;
const channel = socket.channel(`user:${userId}`);
const joinedChannel = channel.join();
// Convert the ok message into a channel join
const ok = receivePattern(joinedChannel, 'ok')
// Just an example of doing some arbitrary mapping since you seem to be doing it
// in your example
.mapTo('something')
.map(channelActions.join);
// Convert the error message
const error = receivePattern(joinedChannel, 'error')
.map(channelActions.error);
// Since the listener methods follow a standard event emitter interface
// You can use fromEvent to capture them
// Rather than the more verbose fromEventPattern we used above
const addRooms = Observable.fromEvent(channel, 'rooms:add')
.map(channelActions.add);
const somethingElse = Observable.fromEvent(channel, 'something:else')
.map(channelActions.somethingElse);
// Merge the resulting stream into one
return Observable.merge(ok, error, addRooms, somethingElse);
});
);
};

Firing multiple actions on catch error

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.