How to chain firebase observables using redux-saga? - reactjs

I've recently made the move from ionic/angular2/Rxjs to React/React Native/Redux-sagas. I'm porting over my app from ionic to react native and have really enjoyed the process. One thing, however, I have really struggled with is using firebase in react native via redux sagas. I can do simple requests however I would like to chain three requests and get the return value as an JSON object. Currently I have this :
export const getTuteeUID = state => state.login.username;
function* getMemberProfiles(UID) {
try {
const memberObject = yield call(get, 'userProfile', UID);
return memberObject;
} catch (err) {
console.log(err);
}
}
function* getLatestConversation(grpID) {
try {
const opts = newOpts();
const refLatestConvo = firebase
.database()
.ref('conversations')
.child(`${grpID}`)
.orderByChild('createdAt')
.limitToLast(1);
yield call([refLatestConvo, refLatestConvo.on], 'value', opts.handler);
while (true) {
const { data } = yield take(opts);
if (data.val()) {
return data.val()[data._childKeys[0]];
}
return null;
}
} catch (err) {
console.log(err);
}
}
export function* getChatGroupObject(grpID, tuteeUID) {
try {
const groupObject = yield call(get, 'groups', grpID);
const memberProfiles = yield Object.keys(groupObject.members)
.filter(mem => mem !== tuteeUID)
.map(memUID => call(getMemberProfiles, memUID));
const latestConversation = yield call(getLatestConversation, grpID);
return { ...groupObject, memberProfiles, key: grpID, latestConversation };
} catch (err) {
console.log(err);
}
}
/**
*
* #return {Generator} []
*/
export function* fetchChatGroups() {
try {
const opts = newOpts();
const tuteeUID = yield select(getTuteeUID);
const refGrpIDs = firebase.database().ref('userProfile').child(`${tuteeUID}/groups`);
const snapGrpIDs = yield call([refGrpIDs, refGrpIDs.once], 'value', opts.handler);
if (snapGrpIDs.val()) {
const groupObject = yield Object.keys(snapGrpIDs.val()).map(grpID =>
call(getChatGroupObject, grpID, tuteeUID),
);
yield put(ChatAction.chatGroupsReceived(groupObject));
} else {
ChatAction.chatGroupsReceived([]);
}
} catch (error) {
console.log(error);
}
}
Now this works and returns the correct object, however if the latest conversation in the array changes the object won't update. How can I get this to continue updating? Another thing is if I were to put this in a while(true) loop, is there a way to unsubscribe from the observable? In rxjs this used to be super easy to do.

Related

How To Know If Dispatch Is Success or No In Redux Saga

So i already create the createMonsterStart and it will hit my API, my API is returning response code, and I want to alert success when the response code is 00 otherwise it will alert failed, how can i achieve that? here is my code:
const onSubmitHandler = () => {
dispatch(createMonsterStart(monster))
if(dispatch success){
alert("success")
}else{
alert("error")
}
}
And here is the redux saga code:
export function* createMonsterAsync({ payload: { monster } }) {
try {
const user = yield select(getUser)
const a = yield call(createMonster, user.user.token, monster)
if (a.error) {
yield put(createMonsterFailure(a.error))
return false
}
const monsters = yield call(fetchMonsterAsync)
yield put(createMonsterSuccess(monsters))
} catch (error) {
yield put(createMonsterFailure(error))
}
}

Is there Promise.any like thing in redux saga?

I know about Redux Saga's all([...effects]) effect combinator that is very similar to Promise.all utility, but I've not found something similar to Promise.any behavior that will:
run all effects at the same time
fail if all effects fail (otherwise succeed)
if fail throw AggregateError of all errors
if succeed return nothing or just first result (from multiple results)
e.g.
export function* getHomeDataSaga() {
yield* any([
call(getTopUsersSaga, { payload: undefined }),
call(getFavoritesSaga, { payload: undefined }),
call(getTrendingTokensSaga, { payload: undefined }),
call(getTopCollectionsSaga, { payload: { itemsPerPage: 9, page: 1 } }),
]);
}
This would be very useful when you want to group multiple (decomposed) sagas in to a single saga, it won't fail-fast but finish all effects.
Answer
Based on Martin Kadlec answer ended up using:
export function* anyCombinator(effects: SagaGenerator<any, any>[]) {
const errors = yield* all(
effects.map((effect) =>
call(function* () {
try {
yield* effect;
return null;
} catch (error) {
return error;
}
}),
),
);
if (errors.every((error) => error !== null)) {
throw new AggregateError(errors);
}
}
There isn't an existing effect that would do that, but you can create your own utility that will do that for you. The any functionality is very similar to the all functionality in that in one case you will get all the results/errors and in the other you get the first one that succeeds/fails. So you can easily get the any functionality by flipping the all effect -> for each item you throw on success and return on error.
const sagaAny = (effects = []) => {
const taskRunner = function* (effect) {
let value;
try {
value = yield effect;
} catch (err) {
// On error, we want to just return it
// to map it later to AggregateError
return err;
}
// On success we want to cancel all the runners
// we do that by throwing here
throw value;
};
return call(function* () {
try {
const runners = effects.map((effect) => call(taskRunner, effect));
// If one of the runners throws on success the all effect will
// cancel all the other runners
const failedResults = yield all(runners);
throw new AggregateError(failedResults, "SAGA_ANY");
} catch (err) {
if (err instanceof AggregateError) throw err;
return err;
}
});
};
function* getHomeDataSaga() {
const result = yield sagaAny([
call(getTopUsersSaga, { payload: undefined }),
call(getFavoritesSaga, { payload: undefined }),
call(getTrendingTokensSaga, { payload: undefined }),
call(getTopCollectionsSaga, { payload: { itemsPerPage: 9, page: 1 } }),
]);
}
In case you would prefer not to cancel the other sagas once one succeeds, things get a bit trickier because in standard fork tree the main saga (e.g. getHomeDataSaga) would wait until all the forked sagas (task runners) are done before continuing. To get around that we can use the spawn effect, which will not block the main saga though it has some other implications (e.g. if you kill them main saga the spawned sagas will continue running).
Something like this should do the trick:
const sagaAny = (effects = []) => {
const taskRunner = function* (effect, resultsChannel) {
try {
value = yield effect;
yield put(resultsChannel, { type: "success", value });
} catch (err) {
yield put(resultsChannel, { type: "error", value: err });
}
};
return call(function* () {
const resultsChannel = yield call(channel);
yield all(
effects.map((effect) => spawn(taskRunner, effect, resultsChannel))
);
const errors = [];
while (errors.length < effects.length) {
const result = yield take(resultsChannel);
if (result.type === "success") {
yield put(resultsChannel, END);
return result.value;
}
if (result.type === "error") errors.push(result.value);
}
throw new AggregateError(errors, "SAGA_ANY");
});
};
I use custom channel here to send the results from the spawned runners to the utility saga so that I can react to each finished runner based on my needs.

How to do fast redirect without missing data with redux saga?

I had a problem when I was redirecting too quickly to another page (these pages would call a different APIs from the server). This leads to Components still rendering while no data is available. My expectation is to delay redirection and only navigate when the data is available. Please check the code, my Saga:
function* fetchLaptops() {
try {
const laptops = yield call(apis.fetchLaptops);
} catch (error) {
console.log(error);
}
}
function* fetchMonitors() {
try {
const monitors = yield call(apis.fetchMonitors);
} catch (error) {
console.log(error);
}
}
function* fetchMouses() {
try {
const mouses = yield call(apis.fetchMouses);
} catch (error) {
console.log(error);
}
}
function* mySaga() {
takeLatest(actions.getLaptops.getLaptopsRequest, fetchLaptops);
takeLatest(actions.getLaptops.getMonitorsRequest, fetchMinitors);
takeLatest(actions.getLaptops.getMousesRequest, fetchMouses);
}
File APIs to fetch data from server, example with laptops:
const fetchLaptops = async () => {
const response = await API.get('/laptops')
return response;
};

Redux saga yield put unit test not working

I looking to doc and some samples online, but still not working. I use Sinon for unit test, and I keep getting this error, stuck on this one so long, can't figure it out.
expected { Object (##redux-saga/IO, combinator, ...) } to deeply equal { Object (##redux-saga/IO, combinator, ...) }
My action
export const loadingStatus = (response) => {
return { type: "LOADING_STATUS", response };
};
My saga
export function* mySampleSaga() {
try {
yield put(loadingStatus('loading'));
yield delay(1000);
const config = yield select(getConfig);
const requestCall = new SendingRequest(config);
const linkRequests = yield select(getLinks);
const response = yield call(
[requestService, requestCall.sample],
"2020-01-01",
"2020-12-21"
);
const result = get(response, 'entities.requests', {});
yield put(success(result));
yield put(loadingStatus('done'));
} catch (error) {
yield put(sendError(error));
yield put(loadingStatus('done'));
}
}
My test
describe('sample saga', () => {
const config = {
sample: "123"
};
const linkRequests = ['12345', '5678910'];
it('should update request status - happy path', () => {
const gen = mySampleSaga();
expect(gen.next().value).to.deep.equal(put(loadingStatus('loading'))); // This keep getting error below
});
it('If saga has error', () => {
const gen = mySampleSaga();
const error = new Error('error');
gen.next();
expect(gen.next().value).to.deep.equal(put(sendError(error)));
expect(gen.next().value).to.deep.equal(put(loadingStatus('done')));
expect(gen.next().done).to.equal(true);
});
});

Use eventChannel to get real time data from Firestore

I would like to sync my data in React with Firestore, each time some data is changed I would like to have an update/refresh of my data.
For fetching the data I have:
function* syncCustomers() {
try {
const response = yield call(fireBaseBackend.getCustomersAPI);
yield put(loadCustomers(response));
} catch (error) {
yield put(customerError(error));
}
}
Where my getCustomersAPI is:
getCustomersAPI = () => {
return new Promise((resolve, reject) => {
firebase.firestore().collection("customers").onSnapshot((snap) => {
let documents = [];
snap.docs.forEach(doc => {
let result = doc.data();
result.uid = doc.id;
documents.push(result);
});
resolve(documents);
});
});
};
This works to get my data, for the real time I implemented:
function* usersChannelSaga () {
try {
while (true) {
const response = yield call(fireBaseBackend.getCustomersAPI);
yield put(loadCustomers(response));
}
}
catch (error) {
yield put(customerError(error));
}
}
But I would like to use the eventChannel for this. How does this need to be implemented?
I tried:
function* usersChannelSaga () {
const channel = eventChannel(emit => fireBaseBackend.syncCustomers(emit));
try {
while (true) {
const data = yield take(channel);
yield put(loadCustomers(response));
}
}
catch (error) {
yield put(customerError(error));
}
}
Where syncCustomers is :
syncCustomers(emit){
return firebase.firestore().collection("customers").onSnapshot(emit);
}
Anyone an idea how to solve this?

Resources