Redux Observables: Custom observer with multiple events? - react-native

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

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/

Issue rendering data from firestore document in react native

I created a map for the array of exercises in my database, and then for each exercise, which is a document reference, I'm getting the data from that document reference and setting it to a state. This is resulting in an infinite loop right now.
If I remove the setExerciseData line, the console logs the exercise object's data that I'm expecting to see. I'm really not sure what the correct way to render the name field from this data is.
{workout.exercises.map((exercise) => {
async function getData(exercise) {
getDoc(exercise).then((doc) => {
console.log(doc.data());
setExerciseData(doc.data());
});
}
getData(exercise);
return (
<Text>{exerciseData.name}</Text>
)
})}
You need to use useEffect() and setState() to be able to render your data. Also, Firebase Firestore is Asynchronous in nature, as a general note, no one should be trying to convert an Async operation into a sync operation as this will cause problems. You need to use an Asynchronous function to fetch data from Firestore. See sample code below:
const getExerciseData = async () => {
const docRef = doc(db, "<collection-name>", '<document-id>')
const docSnap = await getDoc(docRef)
if (docSnap.exists()) {
// console.log("Document data:", docSnap.data())
setExerciseData(docSnap.data())
} else {
// doc.data() will be undefined in this case
console.log("No such document!")
}
}
useEffect(() => {
getExerciseData()
}, [])
return (
<Text>{exerciseData.name}</Text>
)
You could also check my answer on this thread for more use-cases.

How to use setState with ssdp m-search discovery?

I am using SSDP search message to discover devices with connected same network but when i tried to call setState hooks inside client.on function I only get one device informations.
I initialized my state value in this way
const [deviceList, setDeviceList] = useState([]);
And create a function for the client to add deviceList as it is found
const getAllDevices = () => {
var Client = require('react-native-ssdp-remote').Client,
client = new Client();
client.search('urn:dial-multiscreen-org:service:dial:1');
client.on('response', function (headers) {
const url = new URL(headers.LOCATION);
if (url != null) {
if (!deviceList.includes(url)) {
setDeviceList([...deviceList, url]);
}
}
});
};
and called this function inside useEffect
useEffect(() => {
getAllDevices();
}, []);
There are 4 devices connected to same network and it goes into setDeviceList process 4 times but i can only get one device. Could you please support.
Thanks.
I think this is more of a race condition instead of a library issue.
Give it a try with using functional update on setDeviceList.
setDeviceList(deviceList => {
return [...deviceList, url]
}

My saga function is not being called at all when calling my action?

I joined a big/medium project, I am having a hard time creating my first redux-saga-action things, it is going to be a lot of code since they are creating a lot of files to make things readable.
So I call my action in my componentDidMount, the action is being called because I have the alert :
export const fetchDataRequest = () => {
alert("actions data");
return ({
type: FETCH_DATA_REQUEST
})
};
export const fetchDataSuccess = data => ({
type: FETCH_DATA_SUCCESS,
payload: {
data,
},
});
This is my history saga : ( when I call the action with this type, The function get executed )
export default function* dataSaga() {
// their takeEverymethods
yield takeEvery(FETCH_DATA_REQUEST, fetchData);
}
This is what has to be called : ( I am trying to fill my state with data in a json file : mock )
export default function* fetchTronconsOfCircuit() {
try {
// Cal to api
const client = yield call(RedClient);
const data = yield call(client.fetchSomething);
// mock
const history = data === "" ? "" : fakeDataFromMock;
console.log("history : ");
console.log(history);
if (isNilOrEmpty(history)) return null;
yield put(fetchDataSuccess({ data: history }));
} catch (e) {
yield put(addErr(e));
}
}
And this is my root root saga :
export default function* sagas() {
// many other spawn(somethingSaga);
yield spawn(historySaga);
}
and here is the reducer :
const fetchDataSuccess = curry(({ data }, state) => ({
...state,
myData: data,
}));
const HistoryReducer = createSwitchReducer(initialState, [
[FETCH_DATA_SUCCESS, fetchDataSuccess],
]);
The method createSwitchReducer is a method created by the team to create easily a reducer instead of creating a switch and passing the action.type in params etc, their method is working fine, and I did exactly what they do for others.
Am I missing something ?
I feel like I did everything right but the saga is not called, which means it is trivial problem, the connection between action and saga is a common problem I just could not figure where is my problem.
I do not see the console.log message in the console, I added an alert before the try-catch but got nothing too, but alert inside action is being called.
Any help would be really really appreciated.
yield takeEvery(FETCH_DATA_REQUEST, fetchData);
should be
yield takeEvery(FETCH_DATA_REQUEST, fetchTronconsOfCircuit);

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.