Saga is being called multiple times - reactjs

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

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

I need to cancel a particular task in redux saga from the tasks which r running parallely. with the code below all of the parallel tasks are cancelled

function* imageUploadfunctionCall(payload) {
for (let image of payload.payload) {
const {response, error} = yield call(imageUploadRequest(image))
if (response) {
yield put({type: ON_UPLOAD_SUCCESS, payload: image})
} else if (error) {
console.log('error', error)
}
}
}
export function* watchImageUpload() {
while (true) {
let workerTask = yield takeEvery(
ON_UPLOAD_PROGRESS,
imageUploadfunctionCall
)
yield take(ON_CANCEL_BATCH_UPLOAD)
yield cancel(workerTask)
}
}
There is multiple ways you can do this, for example you can use an in-between saga with a race effect:
function* imageUploadfunctionCall(payload) {
for (let image of payload.payload) {
const {response, error} = yield call(imageUploadRequest(image))
if (response) {
yield put({type: ON_UPLOAD_SUCCESS, payload: image})
} else if (error) {
console.log('error', error)
}
}
}
function* imageUploadSaga(payload) {
yield race([
call(imageUploadfunctionCall, payload),
take(a => a.type === ON_CANCEL_BATCH_UPLOAD && a.id === payload.id),
])
}
export function* watchImageUpload() {
yield takeEvery(ON_UPLOAD_PROGRESS, imageUploadSaga)
}
The code above assumes that you send an id property for both the ON_UPLOAD_PROGRESS & ON_CANCEL_BATCH_UPLOAD actions so you can identify which one to cancel.
On a side note, in the upload saga you have:
yield call(imageUploadRequest(image))
which should be probably instead
yield call(imageUploadRequest, image)
(unless imageUploadRequest is a function factory).
For more complex cases you could hold a map of tasks & ids.
export function* watchImageUpload() {
const taskMap = {}
yield takeEvery(ON_CANCEL_BATCH_UPLOAD, function* (action) {
if (!taskMap[action.payload]) return
yield cancel(taskMap[action.payload])
delete taskMap[action.payload]
})
while (true) {
let payload = yield take(ON_UPLOAD_PROGRESS, imageUploadSaga)
const workerTask = yield fork(imageUploadfunctionCall, payload)
taskMap[payload.id] = workerTask
}
}
Again, you need some id in both actions.

What is the best way to implement Loading indicator in redux-saga app?

I'm developing app with (redux + redux-saga + typescript + typesafe-actions + ducks)
I'm considering how to implement loading indicator during api call.
this is my structure
state
ducks
common
actions.ts
reducers.ts
sagas.ts
selectors.ts
group
same as common/
user
same as common/
common/actions
export const beginLoading = () => action(CommonActionTypes.BEGIN_LOADING);
export const endLoading = () => action(CommonActionTypes.END_LOADING);
group/sagas
export function* handleFetchGroup() {
try {
const { group } = yield call(
Api.fetchGroup
);
yield put(addGroup(group));
} catch (err) {
console.error(err);
}
}
user/sagas
export function* handleFetchUsers(action: addGroup) {
try {
yield take(ADD_GROUP);
const users = yield call(
Api.fetchUsers,
action.payload.id
);
yield put(addUsers(users));
} catch (err) {
console.error(err);
}
}
At first, I tried this.
group/sagas
export function* handleFetchGroup() {
try {
yield put(beginLoading()); // added
const { group } = yield call(
Api.fetchGroup
);
for(const group of groups) {
yield put(addGroup(group));
}
yield put(endLoading()); // added
} catch (err) {
yield put(endLoading());
console.error(err);
}
}
in this case, the loading indicator shows only during fetching group data.
so I tried this.
export function* handleFetchGroup() {
try {
yield put(beginLoading());
const { groups } = yield call(
Api.fetchGroups
);
for(const group of groups) {
yield put(addGroup(group));
}
} catch (err) {
yield put(endLoading());
console.error(err);
}
}
user/sagas
export function* handleFetchUsers(action: addGroup) {
try {
yield take(ADD_GROUP);
const users = yield call(
Api.fetchUsers,
action.payload.id
);
yield put(addUsers(users));
yield put(endLoading()); // added
} catch (err) {
console.error(err);
yield put(endLoading());
}
}
in this case,the loading indicator disappeared when the first fetchUsers finished.
Finally I tried this.
export function* handleFetchGroup() {
try {
yield put(beginLoading());
const { groups } = yield call(
Api.fetchGroups
);
for(const group of groups) {
const users = yield call(Api.fetchUsers, group.id); // add
yield put(addUsers(users));
}
yield put(endLoading());
} catch (err) {
yield put(endLoading());
console.error(err);
}
}
But in this case, group saga depends on user entity.
So I want to avoid this, but I didn't come up with it.
Do you have any solution??
I suggest to have isLoading property in user state and group state. When action beginLoading for group is emitted (I assume, that this action will trigger handleFetchGroup), set isLoading: true in group reducer. When group loading is finished, set isLoading: false in group reducer.
Exactly the same logic implement for user. So isLoading which exist in user reducer will be set in response to beginLoading for user and so on.
In component, which will display loading indicator have isLoading from group and isLoading from user and show loading indicator whenever either of two is true.
For example (not tested, use as hint)
const LoadingIndicator: FC<{ user: UserState, group: GroupState }> = ({group, user}) => (
group.isLoading || user.isLoading ? <div>Loading...</div> : null
}
export default connect(state => ({ user: state.user, group: state.group }), {})(LoadingIndicator)
I also see that you're triggering handleFetchUsers in response to ADD_GROUP. This also better to decouple. You may consider creating additional saga (for example loadAll) which will trigger user load in response to group load finish.
For example (not tested)
function *loadAll() {
yield put(BEGIN_GROUP_LOAD)
yield takeEvery(GROUP_LOAD_FINISHED, function*(action) => {
yield handleFetchUsers(action)
}
}

Why cannot catch error in redux-saga generator?

Here is store/store.js
...
const initSagaMiddleware = createSagaMiddleware();
const middlewares = [initSagaMiddleware, fetchPhotosMiddleware, putPhotoMiddleware];
const middlewareEnhancer = applyMiddleware(...middlewares);
...
initSagaMiddleware.run(rootSaga);
export default store;
sagas/api-saga.js
export default function* rootSaga() {
yield all([
photoWatcher()
]);
}
function* photoWatcher() {
yield takeEvery(PUT_PHOTO, putPhotoWorker);
}
function* putPhotoWorker(action) {
try {
const payload = yield call(putPhoto, action.urlParams, action.body);
yield put({ type: PHOTO_UPDATED, payload });
} catch (err) {
yield put({ type: API_ERROR_PUT_PHOTO, payload: err });
}
}
and services/api.js
export function putPhoto(urlParams, body) {
return axios.put('/abc/' + urlParams.key + '/def/' + urlParams.id + '/edit', body)
.then(res => {
return res.data;
})
.catch(err => err);
}
My problem is: even when I get error from an api call, redux-saga putting PHOTO_UPDATED instead of API_ERROR_PUT_PHOTO. What am I doing wrong? How to catch error?
The problem is you are trying to catch the same error twice. Just remove the catch form the putPhoto function and it should work.
export function putPhoto(urlParams, body) {
return axios.put('/abc/' + urlParams.key + '/def/' + urlParams.id + '/edit', body)
.then(res => {
return res.data;
});
}
Both then and catch promises in putPhoto func were resulting in try statement. Because of that, I give up on try...catch, instead I'm using if...else statement now. Like this:
function* putPhotoWorker(action) {
const payload = yield call(putPhoto, action.urlParams, action.body);
if (isResponseTypeWhatIExpected) {
yield put({ type: PHOTO_UPDATED, payload });
} else {
yield put({ type: API_ERROR_PUT_PHOTO, payload });
}
}

Redux saga multiple api calls

I have a bunch of action watchers of my api calls using redux-saga. The thing is I would like to make ONE action watcher which fire all these action watchers to fetch all the api without having to repeat the codes I already have. If one of the watcher return a rejected Promise, it should cancel all the other watchers. What's the best way of doing this?
function* watchFetchUsers() {
while(true) {
yield take([FETCH_USERS]);
try {
const users = yield call(api.fetchUserData);
yield put({ type:FETCH_USERS, payload: users });
} catch (err) {
yield put({ type:SIGNOUT, status: err });
return err;
}
}
}
function* watchFetchDepartments() {
while(true) {
yield take([FETCH_DEPARTMENTS]);
try {
const departments = yield call(api.fetchDepartmentData);
yield put({ type:FETCH_DEPARTMENTS, payload: departments });
} catch (err) {
yield put({ type:SIGNOUT, status: err });
return err;
}
}
}
function* watchFetchPositions() {
while(true) {
yield take([FETCH_POSITIONS]);
try {
const positions = yield call(api.fetchPositionData);
yield put({ type:FETCH_POSITIONS, payload: positions });
} catch (err) {
yield put({ type:SIGNOUT, status: err });
return err;
}
}
}
function* watchFetchBanks() {
while(true) {
yield take([FETCH_BANKS]);
try {
const banks = yield call(api.fetchBankData);
yield put({ type:FETCH_BANKS, payload: banks });
} catch (err) {
yield put({ type:SIGNOUT, status: err });
return err;
}
}
}
function* watchFetchAuthenticatedUser() {
while(true) {
yield take([FETCH_AUTHENTICATED_USER]);
try {
const user = yield call(api.fetchAuthenticatedUser);
yield put({ type:FETCH_AUTHENTICATED_USER, payload: user });
} catch (err) {
yield put({ type:SIGNOUT, status: err });
return err;
}
}
}
export default function* fetchData() {
yield [
fork(watchFetchUsers),
fork(watchFetchDepartments),
fork(watchFetchPositions),
fork(watchFetchBanks),
fork(watchFetchAuthenticatedUser)
];
}
How about this
export function* watchFetchAll() {
while(true) {
// const {type} = yield take(['FETCH_A', 'FETCH_B', ...]);
const {type} = yield take(action => /^FETCH_/.test(action.type));
console.log('type %s', type);
try {
const data = yield call(api.fetch, type);
console.log('data', data);
yield put({type, payload: data})
}
catch (error) {
console.log('error', error);
yield put({ type: 'SIGNOUT', status: error })
}
}
}
export default function* fetchData() {
yield *watchFetchAll();
}
The simple api implementation:
const api = {
fetch(type) {
switch (type) {
case 'FETCH_A': return Promise.resolve({result: 'Fetched A type'});
case 'FETCH_B': return Promise.resolve({result: 'Fetched B type'});
// other cases
default: console.log(`Unknown type ${type}`);
}
}
};
The forked task's error is propagated to parent tasks.
I'm not sure if the below is what you want. But maybe it will work.
function* watchFetchUsers() {
while(true) {
yield take([FETCH_USERS]);
const users = yield call(api.fetchUserData);
yield put({ type:FETCH_USERS, payload: users });
}
}
function* watchFetchDepartments() {
while(true) {
yield take([FETCH_DEPARTMENTS]);
const departments = yield call(api.fetchDepartmentData);
yield put({ type:FETCH_DEPARTMENTS, payload: departments });
}
}
function* watchFetchPositions() {
while(true) {
yield take([FETCH_POSITIONS]);
const positions = yield call(api.fetchPositionData);
yield put({ type:FETCH_POSITIONS, payload: positions });
}
}
function* watchFetchBanks() {
while(true) {
yield take([FETCH_BANKS]);
const banks = yield call(api.fetchBankData);
yield put({ type:FETCH_BANKS, payload: banks });
}
}
function* watchFetchAuthenticatedUser() {
while(true) {
yield take([FETCH_AUTHENTICATED_USER]);
const user = yield call(api.fetchAuthenticatedUser);
yield put({ type:FETCH_AUTHENTICATED_USER, payload: user });
}
}
export default function* fetchData() {
while (true) {
let tasks;
try {
tasks = yield [
fork(watchFetchUsers),
fork(watchFetchDepartments),
fork(watchFetchPositions),
fork(watchFetchBanks),
fork(watchFetchAuthenticatedUser)
];
yield join(...tasks)
} catch (e) {
yield cancel(...tasks);
yield put({ type:SIGNOUT, status: err });
}
}
}
Or if you don't want to restore tasks,
//....
export default function* fetchData() {
try {
yield [
fork(watchFetchUsers),
fork(watchFetchDepartments),
fork(watchFetchPositions),
fork(watchFetchBanks),
fork(watchFetchAuthenticatedUser)
];
} catch (e) {
yield put({ type:SIGNOUT, status: err });
}
}
The answer called Parallel Task wherein you call 2 or more api at the same time, also efficient to handle than to this (example below)
// wrong, effects will be executed in sequence
const users = yield call(fetch, '/users')
const repos = yield call(fetch, '/repos')
//instead, try this one
import { all, call } from 'redux-saga/effects'
const [users, repos] = yield all([
call(fetch, '/users'),
call(fetch, '/repos')
])
for more information regards with your issue, I prefer to read more about sync and async of redux saga, Redux-Saga Parallel task and Non-Blocking Call

Resources