retry functionality in redux-saga - reactjs

in my application I have the following code
componentWillUpdate(nextProps) {
if(nextProps.posts.request.status === 'failed') {
let timer = null;
timer = setTimeout(() => {
if(this.props.posts.request.timeOut == 1) {
clearTimeout(timer);
this.props.fetchData({
page: this.props.posts.request.page
});
} else {
this.props.decreaseTimeOut();
}
}, 1000);
}
}
What it does is that, when an API request encountered an error maybe because there is no internet connection (like how facebook's chat works), or there was an error in the back-end, it would retry after five seconds, but the setTimeout needs to be set every one second to update a part of the store, i.e., the line this.props.decreaseTimeOut();, but if the counter has run out, so five seconds have passed, the if block would run and re-dispatch the fetchData action.
It works well and I have no problem with it, at least in terms of functionality, but in terms of code design, I know that it's a side-effect and it should not be handled in my react-component, and since I'm using redux-saga (but I'm new to redux-saga, I just learned it today), I want to transform that functionality into a saga, I don't quite have an idea as to how to do that yet, and here is my fetchData saga by the way.
import {
take,
call,
put
} from 'redux-saga/effects';
import axios from 'axios';
export default function* fetchData() {
while(true) {
try {
let action = yield take('FETCH_DATA_START');
let response = yield call(axios.get, '/posts/' + action.payload.page);
yield put({ type: 'FETCH_DATA_SUCCESS', items: [...response.data.items] });
} catch(err) {
yield put({ type: 'FETCH_DATA_FAILED', timeOut: 5 });
}
}
}

The less intrusive thing for your code is using the delay promise from redux-saga:
catch(err) {
yield put({ type: 'FETCH_DATA_FAILED'});
for (let i = 0; i < 5; i++) {
yield call(delay, 1000);
yield put(/*Action for the timeout/*);
}
}
But I'd refactor your code in this way:
function* fetchData(action) {
try {
let response = yield call(axios.get, '/posts/' + action.payload.page);
yield put({ type: 'FETCH_DATA_SUCCESS', items:[...response.data.items] });
} catch(err) {
yield put({ type: 'FETCH_DATA_FAILED'});
yield put({ type: 'SET_TIMEOUT_SAGA', time: 5 });
}
}
}
function *setTimeoutsaga(action) {
yield put({type: 'SET_STATE_TIMEOUT', time: action.time}); // Action that update your state
yield call(delay, 1000);
// Here you use a selector which take the value if is disconnected:
// https://redux-saga.js.org/docs/api/#selectselector-args
const isStillDisconnected = select()
if (isStillDisconnected) {
yield put({type: 'SET_TIMEOUT_SAGA', time: action.time - 1});
}
function *fetchDataWatchers() {
yield takeEvery('FETCH_DATA_START', fetchData);
yield takeEvery('SET_TIMEOUT_SAGA', setTimeoutSaga);
// You can insert here as many watcher you want
}
export default [fetchDataWatchers]; // You will use run saga for registering this collection of watchers

Related

Redux saga dispatching actions from inside map inside saga-action

I have API calls in the following manner:
Call main api which gives me an array of objects.
For each object inside array I have to call another API asynchronously.
As soon as the sub API call for the object is finished, update its data in redux store which is an array (ofc) and show it.
So the scenario is a list showing items which grow in dynamic fashion.
Since I am using redux-saga, I have to dispatch the second part from inside an redux-action. I have tried the follow way:
const response = yield call(get, 'endpoint')
const configHome = response.map(function* (ele) {
const data = yield call(get, ele.SomeURI + '?someParameter=' + ele.someObject.id)
}))
This doesn't work since map doesn't know anything about generator functions. So I tried this:
const response = yield call(get, 'endpoint')
const configHome = yield all(response.map((ele) => {
return call(get, paramsBuilder(undefined, ele.CategoryID))
}))
But this will block my UI from showing available data untill all sub API calls are finished.
I have also tried making a separate generator function which I call from inside map and call its .next() function but the problem here again is that saga doesn't control that generator function so the call effect doesn't return any value properly.
Completely stuck on this part. Would appreciate any help.
Have you tried this, I have created a sample this might help
import { put, takeLatest, all, call } from 'redux-saga/effects';
function* _fetchNews(id) {
const data = yield fetch(
`https://jsonplaceholder.typicode.com/todos/${id}`
).then(function(response) {
const data = response.json();
return data;
});
console.log(id);
yield put({ type: 'NEWS_RECEIVED', data });
return data;
}
function* _getData() {
const json = yield fetch('https://jsonplaceholder.typicode.com/todos').then(
response => response.json()
);
return json;
}
function* fetchNews() {
const json = yield _getData();
const configHome = json.map(ele => _fetchNews(ele.id));
for (var item of configHome) {
yield item;
}
}
function* actionWatcher() {
yield takeLatest('GET_NEWS', fetchNews);
}
export default function* rootSaga() {
yield all([actionWatcher()]);
}
yield all - the generator is blocked until all the effects are resolved or as soon as one is rejected
So you will need to dispatch events separately for each sub API
Let's assume you have 2 actions:
export const getMainApi =() => ({
type: types.GET_MAIN_API,
});
export const getSubApi = endpoint => ({
type: types.GET_SUB_API,
endpoint,
});
Then your operations will be:
const get = endpoint => fetch(endpoint).then(response => response);
function* fetchMainApi(action) {
try {
const response = yield call(get, 'endpoint');
for (let i = 0; i < response.length; i += 1) {
// dispatch here all sub APIs
yield put(
getSubApi(
response[i].SomeURI + '?someParameter=' + response[i].someObject.id,
),
);
}
} catch (e) {
console.log(e);
}
}
function* fetchSubApi(action) {
try {
const response = yield call(get, action.endpoint);
yield put({
type: types.RECEIVE_SUB_API,
response
});
} catch (e) {
console.log(e);
}
}
takeLatest(type.GET_MAIN_API, fetchMainApi)
takeEvery(types.GET_SUB_API, fetchSubApi)
So on success of receiving sub APIs you need to insert data into your state inside reducers.
This is just pseudo code.

A 'yield' expression is only allowed in a generator body

I'm using redux-saga to fetch server api data.
My question is that I'm trying to design following code.
However yield put(get01Action(members)); which is commented out has following Syntax error.
A 'yield' expression is only allowed in a generator body.
I don't know how to manage it.
import '#babel/polyfill';
import { fork, take, put } from 'redux-saga/effects';
import axios from "axios";
export function* rootSaga(){
yield fork(fetch01);
yield fork(fetch02);
}
function* fetch01() {
while (true){
yield take('FETCH01_REQUEST');
axios.get('/api/members')
.then(function (response) {
// handle success
let members = response.data.data;
// yield put(get01Action(members));
})
.catch(function (error) {
// handle error
console.log(error);
})
.finally(function () {
// always executed
});
}
}
function* fetch02() {
....
}
function get01Action(members){
return {
type: 'GET_MEMBERS',
member_id: 0,
members: members
}
}
Please give me some advice.
Thanks.
Because your generator fetch01 is sync but you're waiting an Promise to be resovled.
yield can not be wrapped in other functions other than generators.
You can make the generator async, like this:
export async function* rootSaga(){
yield await fork(fetch01);
yield fork(fetch02);
}
async function* fetch01() {
while (true) {
yield take('FETCH01_REQUEST');
try {
const response = await axios.get('/api/members');
// handle success
let members = response.data.data;
yield put(get01Action(members));
} catch (error) {
// handle error
console.log(error);
} finally {
// always executed
}
}
}
you can use call effect to make axios call and then you will be able to use put.
right now it's not working because you are using yield inside call back of promise.
function* fetch01() {
while (true){
try {
yield take('FETCH01_REQUEST');
const response = yield call(axios.get, '/api/members');
yield put(get01Action(response.data.data))
} catch(err) {
console.error(err)
}
}

Saga is being called multiple times

How to stop saga being called multiple times. Once i dispatched an action i received the result multiple times. i don't know what i'm missing here.
Saga.js
export function* watchRegisterUser() {
yield takeLatest(REGISTER_USER, registerWithEmailPassword);
}
export function* watchLoginUser() {
const logi = yield takeLatest(LOGIN_USER, loginWithEmailPassword);
}
export function* watchLogoutUser() {
yield takeLatest(LOGOUT_USER, logout);
}
export default function* rootSaga() {
yield all([
fork(watchLoginUser),
fork(watchLogoutUser),
fork(watchRegisterUser)
]);
}
When i put wrong credentials i get the response and notification is displayed, working fine for the first time. But when i change the credentials, i get notifications (multiple times). Even when state
Reference: Same problem here
I'm not sure what am i missing here.
Thanks
Update
function* loginWithEmailPassword({ payload }) {
const { history } = payload;
try {
const response = yield call(loginWithEmailPasswordAsync, payload.user);
console.log("login :", response);
if (response.status >= 200 && response.status < 300) {
const loginUser = yield response.json();
localStorage.setItem("access_token", loginUser.access_token);
yield put(loginUserSuccess(loginUser));
history.push("/");
} else if (response.status === 400) {
yield put({
type: LOGIN_USER_FAILURE,
error: "Email and Password are wrong!"
});
}
} catch (error) {
yield put({ type: LOGIN_USER_FAILURE, error: "Something went wrong!" });
}
}
you could try debounce to get around this, in the example below I debounce repeated actions withing a 500ms window.
function* createAutocompleteLists(action) {
const state = yield select()
if (!state.users.loaded && !state.users.loading) {
yield put(getUsers())
yield take([GET_SUCCESS, GET_FAIL])
}
yield call(parseAutocomplete, state.couchdbSavedSearch.current_share_with, state)
}
function* createAutocompleteForwardLists(action) {
const state = yield select()
if (!state.users.loaded && !state.users.loading) {
yield put(getUsers())
yield take([GET_SUCCESS, GET_FAIL])
}
yield call(parseForwardAutocomplete, state.forward.current_forward_to, state)
}
function* debounceCurrentShareWith () {
yield debounce(500, [GET_SUCCESS, SET_CURRENT_SHARE_WITH, SPACE_SET_CURRENT_SHARE_WITH], createAutocompleteLists)
}
function* debounceCurrentShareWithForward () {
yield debounce(500, [GET_SUCCESS, FORWARD_SET_CURRENT_FORWARD_TO], createAutocompleteForwardLists)
}
export default function* usersSagas() {
yield spawn(debounceCurrentShareWith)
yield spawn(debounceCurrentShareWithForward)
}

How can i cancel or update an saga polling request with same action call

I am polling api with delay and cancellation using race condition.
What i want to achieve is when my page renders, i want to start polling for scores and on some action i will call same polling (same api) with some other query params so that i just want to update the same polling with new params.
for that i have done something like this.
export function* pollScoreSnippets() {
while (true) {
try {
const { data } = yield call(() => request(apis.GET_SCORE_API));
yield put({
type: types.DASHBOARD_DATA_FETCHED,
payload: {
type: ['scores'],
data: {
scores: { values: data.data },
},
},
});
yield call(delay, SCORE_SNIPPET_POLLING_DELAY);
} catch (err) {
yield put({
type: types.DASHBOARD_DATA_FETCHING_ERROR,
payload: {
error: err.response.data,
},
});
yield call(delay, SCORE_SNIPPET_POLLING_DELAY + 10);
}
}
}
export function* watchPollSaga() {
while (true) {
// console.log('watching');
yield take(types.POLL_SCORE_SNIPPETS);
yield race([call(dashGenerators.pollScoreSnippets), take(types.STOP_POLLING_SCORE_SNIPPETS)]);
}
}
That is working for me but with this approach i have to call cancel action then again same action for restarting polling again.
Is there any way that if i will call same action and either it will update the current polling request or cancel or restart with new polling request
something kind of:
export function* watchPollSaga() {
while (true) {
// console.log('watching');
yield take(types.POLL_SCORE_SNIPPETS);
yield race([call(dashGenerators.pollScoreSnippets), take(types.POLL_SCORE_SNIPPETS)]);
}
}

Redux saga, axios and progress event

Is there clean/short/right way to using together axios promise and uploading progress event?
Suppose I have next upload function:
function upload(payload, onProgress) {
const url = '/sources/upload';
const data = new FormData();
data.append('source', payload.file, payload.file.name);
const config = {
onUploadProgress: onProgress,
withCredentials: true
};
return axios.post(url, data, config);
}
This function returned the promise.
Also I have a saga:
function* uploadSaga(action) {
try {
const response = yield call(upload, payload, [?? anyProgressFunction ??]);
yield put({ type: UPLOADING_SUCCESS, payload: response });
} catch (err) {
yield put({ type: UPLOADING_FAIL, payload: err });
}
}
I want to receive progress events and put it by saga. Also I want to catch success (or failed) result of the axios request. Is it possible?
Thanks.
So I found the answer, thanks Mateusz BurzyƄski for the clarification.
We need use eventChannel, but a bit canningly.
Suppose we have api function for uploading file:
function upload(payload, onProgress) {
const url = '/sources/upload';
const data = new FormData();
data.append('source', payload.file, payload.file.name);
const config = {
onUploadProgress: onProgress,
withCredentials: true
};
return axios.post(url, data, config);
}
In saga we need to create eventChannel but put emit outside.
function createUploader(payload) {
let emit;
const chan = eventEmitter(emitter => {
emit = emitter;
return () => {}; // it's necessarily. event channel should
// return unsubscribe function. In our case
// it's empty function
});
const uploadPromise = upload(payload, (event) => {
if (event.loaded.total === 1) {
emit(END);
}
emit(event.loaded.total);
});
return [ uploadPromise, chan ];
}
function* watchOnProgress(chan) {
while (true) {
const data = yield take(chan);
yield put({ type: 'PROGRESS', payload: data });
}
}
function* uploadSource(action) {
const [ uploadPromise, chan ] = createUploader(action.payload);
yield fork(watchOnProgress, chan);
try {
const result = yield call(() => uploadPromise);
put({ type: 'SUCCESS', payload: result });
} catch (err) {
put({ type: 'ERROR', payload: err });
}
}
I personally found the accepted answer to be very convoluted, and I was having a hard time implementing it. Other google / SO searches all led to similar type answers. If it worked for you, great, but I found another way using an EventEmitter that I personally find much simpler.
Create an event emitter somewhere in your code:
// emitter.js
import { EventEmitter } from "eventemitter3";
export default new EventEmitter();
In your saga to make the api call, use this emitter to emit an event within the onUploadProgress callback:
// mysagas.js
import eventEmitter from '../wherever/emitter';
function upload(payload) {
// ...
const config = {
onUploadProgress: (progressEvent) = {
eventEmitter.emit(
"UPLOAD_PROGRESS",
Math.floor(100 * (progressEvent.loaded / progressEvent.total))
);
}
};
return axios.post(url, data, config);
}
Then in your component that needs this upload progress number, you can listen for this event on mount:
// ProgressComponent.jsx
import eventEmitter from '../wherever/emitter';
const ProgressComponent = () => {
const. [uploadProgress, setUploadProgress] = useState(0);
useEffect(() => {
eventEmitter.on(
"UPLOAD_PROGRESS",
percent => {
// latest percent available here, and will fire every time its updated
// do with it what you need, i.e. update local state, store state, etc
setUploadProgress(percent)
}
);
// stop listening on unmount
return function cleanup() {
eventEmitter.off("UPLOAD_PROGRESS")
}
}, [])
return <SomeLoadingBar value={percent} />
}
This worked for me as my application was already making use of a global eventEmitter for other reasons. I found this easier to implement, maybe someone else will too.

Resources