I'm combining a set of Sagas, ( some takeEvery and takeLatest ) from different modules and using yield all(sagas) to combine them all in RootSaga.
Everything works, without issue. I catch errors inside the Sagas themselves. But, now the requirement is to catch errors at RootSaga level too, in any case, someone misses catching a problematic part.( Actually, I'm working on boilerplate for a multi-team project. )
I see, if someone is not using try catch to catch a problematic part, then the error is propagated and Saga completely stops working after that. Sagas won't watch anything thereafter.
What I want to do is, let other Sagas run without issues and RootSaga to be working thereafter, keep watching as usual. How can I achieve this?
export default function* rootSaga() {
yield all(sagas);
}
For v1 use
rootSagaTask.toPromise().catch(e => {
});
When you run the rootSaga you are getting back a task. That task has a done property which is a promise. So:
const rootSagaTask = reduxSagaMiddleware.run(rootSaga);
rootSagaTask.done.catch(error => {
// Error here is a fatal error.
// None of the sagas down the road caught it.
});
After much thinking about this problem, I implemented my own middleware function. Like:
Middleware function:
* #function errorFallback
* #param {CallableFunction} sagaFunction
* #param {object} action
* #description
* * Captured exceptions which is occurred from saga function.
* * Whenever any exception occurred from saga functions, whole sagas will be destroyed.
* * We can use this function to save sagas from die.
* #example
* export function* getSagaData() {
* yield all([
* takeLatest('GET_SAGA_DATA', errorFallback, getSaga),
* ]);
* }
*/
function* errorFallback(sagaFunction, action) {
try {
yield sagaFunction(action);
} catch (error) {
// exception log to your server
}
}
export default errorFallback;
Uses
export function* loadSampleSaga() {
try {
const response = yield post('/create', {
name: 'test',
salary: '123',
age: '23',
});
yield put(sampleSuccessAction());
} catch (err) {
yield put(sampleFailureAction());
throw err;
}
}
export function* getSampleSaga() {
yield all([takeLatest('GET_SAMPLE_SAGA_DATA', errorFallback, loadSampleSaga)]);
}
Related
I'm not quite sure why takeLeading isn't working for me (takeLeading is supposed to take the first call and ignore subsequent calls until the first is returned). It's calling the same call 3 separate times like a takeEvery with the same parameters from 3 separate components in their useEffect(() => {getApiWatcher(params)}, []) on mount hook. It appears those don't return before the second is called either so I know it's not 3 uniquely separate calls.
function getApi(params) {
console.log('GET CALL') // called 3 times in console and network tab
return Api.doCall(
`API/${params.number}/${params.type}`,
'GET'
);
}
function* getApiActionEffect(action) {
const { payload } = action;
try {
const response = yield call(getApi, payload);
yield put(getApiSuccess({ data: response.data, status: response.status }));
} catch (e) {
yield put(getApiError(e.response));
}
}
export function* getApiActionWatcher() {
yield takeLeading( // should make sure only first call is made and subsequent are ignored
GET_API_WATCHER,
getApiActionEffect
);
}
// action
export function getApiWatcher(payload) {
return { type: GET_API_WATCHER, payload };
}
// passed dispatch as props
const mapDispatchToProps = (dispatch) => bindActionCreators( { getApiWatcher, }, dispatch );
// root saga
export default function* rootSaga() {
yield all([... getApiActionWatcher(),...])
}
There is a lot more code involved so I'm not creating a sample jsfiddle, but ideas for what could potentially be going wrong are what I'm looking for! Might have over looked something.
Turns out there were duplicate imported functions in the root saga. For example:
// root saga
export default function* rootSaga() {
yield all([
... getApiActionWatcher(),...
... getApiActionWatcher(),...
])
}
Removing the duplicates solved the issue. It also removed other duplicate calls I wasn't working on.
If you want to handle every GET_API_WATCHER action then you better takeEvery redux-saga helper.
And docs says that task spawned with takeLeading blocks others tasks until it's done.
takeLeading is working as intended. The calls to getApi instantly returns.
The only way your code would work the way you want it to is if getApi returns a Promise. If getApi() were to return a Promise, the getApiActionEffect would block until the Promise resolved.
I have a networkSaga, where I fetch posts, add and remove likes. After I added or removed a like, I need to call getPosts to update the number of likes.
In redux-thunk, I'll simply call dispatch(getPosts()) after I added or removed the like. Since, I'm new to sagas, I'm concerned, how should it be done?
import { all, call, fork, put, takeEvery } from 'redux-saga/effects';
import { NetworkActionTypes } from './types';
import { apiCaller } from '../../utils/apiCaller';
import { onSuccess, onFailure } from '../../utils/actionCreators';
function* getPosts(action): Generator {
try {
const res = yield call(apiCaller, action.meta.method, action.meta.route, action.meta.data);
yield put(onSuccess(NetworkActionTypes.GET_POSTS_SUCCESS, res));
} catch (err) {
yield put(onFailure(NetworkActionTypes.GET_POSTS_ERROR, err));
}
}
function* addLike(action): Generator {
try {
const res = yield call(apiCaller, action.meta.method, action.meta.route, action.meta.data);
yield put(onSuccess(NetworkActionTypes.ADD_LIKE_SUCCESS, res));
} catch (err) {
yield put(onFailure(NetworkActionTypes.ADD_LIKE_ERROR, err));
}
}
function* removeLike(action): Generator {
try {
const res = yield call(apiCaller, action.meta.method, action.meta.route, action.meta.data);
yield put(onSuccess(NetworkActionTypes.REMOVE_LIKE_SUCCESS, res));
} catch (err) {
yield put(onFailure(NetworkActionTypes.REMOVE_LIKE_ERROR, err));
}
}
/**
* #desc Watches every specified action and runs effect method and passes action args to it
*/
function* watchFetchRequest(): Generator {
yield takeEvery(NetworkActionTypes.GET_POSTS, getPosts);
yield takeEvery(NetworkActionTypes.ADD_LIKE, addLike);
yield takeEvery(NetworkActionTypes.REMOVE_LIKE, removeLike);
}
/**
* #desc saga init, forks in effects, other sagas
*/
export function* networkSaga() {
yield all([fork(watchFetchRequest)]);
}
The question may sound foolish, but I'll be grateful if you point me to the solution. Thanks!
you can do by yield getPosts(action json you have to pass here) once you receive the success from add/remove api.
But i suggest better to trigger a action from the component which does that for you,
This will help you to keep all the generator functions re-usable and testable.
I have a Saga like so (some pseudocodish).
Saga1 calls an API. Based on the result, I need to call two further APIs. If all the APIs succeed, I call onSuccess, else onFailure.
The code seems to almost work fine, but not quite. The problem I facing with yield all is that it considered saga2 and saga3 complete as soon as the first yield put was called (see comment in saga2/3). It didn't wait for the fetch yield to finish.
I think it's partly due to my misunderstanding of what a "complete effect" means. But apart from that, I want yield all to wait until everything is done. I want any exceptions thrown by fetch in saga2/3 to be caught by catch in saga1.
saga1(action) {
const { onSuccess, onFailure } = action.payload;
try {
yield fetch...
if(response.some_condition) yield all([
put(saga2()),
put(saga3())
])
onSuccess();
}
catch(e) {
onFailure();
}
}
saga2(action) {
yield put(someaction()) // This yields
yield fetch...
}
saga3(action) {
yield put(someaction()) // This yield
yield fetch...
}
This code below is related to my comment below about catch not working
action1 () { // action2 is same
try {
yield fetch();
yield put(FINISHED_1);
}
catch(e) {
throw (e);
}
}
saga1() {
try {
yield put(action1());
yield put(action2());
yield all([
take(FINISHED_1),
take(FINISHED_2),
])
console.log("this doesn't print if exception in either action");
}
catch(e) {
console.log("this doesn't print if exception in either action");
}
finally {
console.log("this prints fine");
}
}
1) To wait for multiple call effects to run to completion:
yield all([
call(saga2, arg1, arg2, ...),
call(saga3, arg1, arg2, ...)
]);
2) To dispatch multiple actions and wait for their success actions to be dispatched:
yield put(action1());
yield put(action2());
yield all([
take(ACTION_1_SUCCESS),
take(ACTION_2_SUCCESS)
]);
Edits responding to comments
If you call the sagas directly with all (#1 above), then you can catch errors conventionally
try {
yield all([
call(saga2, arg1, arg2, ...),
call(saga3, arg1, arg2, ...)
]);
} catch (e) {
// ...
}
But if a saga puts actions that other sagas listen on, that saga does not receive those exceptions. Saga1 is not a parent of or attached to those sagas. It just dispatches actions, and some other tasks elsewhere listen and respond.
For Saga1 to be aware of errors in those sagas, the sagas should not throw errors, but instead dispatch an action with an error payload:
function* saga2(action) {
try {
const result = yield call(...);
yield put(action2Success(result));
} catch (e) {
yield put(action2Failure(e.message));
}
}
A saga that triggers saga2 (via put(action2())) can handle success and failure:
function* saga1(action) {
yield put(action2());
yield put(action3());
const [success, failure] = yield race([
// if this occurs first, the race will exit, and success will be truthy
all([
take(ACTION_2_SUCCESS),
take(ACTION_3_SUCCESS)
]),
// if either of these occurs first, the race will exit, and failure will be truthy
take(ACTION_2_FAILURE),
take(ACTION_3_FAILURE)
]);
if (failure) {
return;
}
// ...
}
Sagas should handle exceptions and update the store with an error state, not throw errors. Throwing errors in sagas gets messy when working with saga concurrency constructs. For example, you cannot directly catch an error thrown by a forked task. Also, using actions to signal saga results keeps a good event log in your store, on which other sagas/reducers can respond to. When you call other sagas, the action that is supposed to initiate that saga (e.g. takeEvery(THE_ACTION, ...)) doesn't get dispatched.
I have a simple React / Redux / Redux Sagas app which uses an API to show a random picture of a dog upon clicking a button.
dogSagas.js
import { put, call, takeEvery, all } from 'redux-saga/effects';
import * as types from '../actions/types';
import * as actions from '../actions/dogActions';
import { DOG_API } from '../constants/variables';
function* getDogAsync() {
try {
yield put(actions.getDog);
const data = yield call(() =>
fetch(DOG_API)
.then(res => res.json())
.catch(err => console.error(err)),
);
yield put(actions.getDogOk(data));
} catch (error) {
yield put(actions.getDogFail());
}
}
function* watchGetDog() {
yield takeEvery(types.GET_DOG, getDogAsync);
}
export default function* rootSaga() {
yield all([watchGetDog()]);
}
dogActions.js
import * as types from '../actions/types';
export const getDog = () => ({
type: types.GET_DOG,
});
export const getDogOk = data => ({
type: types.GET_DOG_OK,
payload: data.message,
});
export const getDogFail = () => ({
type: types.GET_DOG_FAIL,
});
errors
However, I have two different errors.
1.) When I do yield put(actions.getDog); the app works, but in the console I get the error:
uncaught at getDogAsync Error: Actions must be plain objects. Use custom middleware for async actions.
2.) If instead I do: yield put(actions.getDog()); the app consumes a lot of CPU and effectively crashes.
questions
So, my questions are:
1.) Why does this approach cause Redux to complain about non-plain objects?
2.) Why does this seemingly innocuous statement cause the app to crash?
full code
Full code on StackBlitz here.
The issue was that I was calling the getDog() action creator within the async generator getDogAsync(). Since we had a watcher, this was leading to an infinite number of calls to getDog().
So, to fix, just remove:
yield put(actions.getDog);
within the getDogAsync().
I'm trying to implement React-boilerplate with redux-saga inside. So i'm trying to fetch some data from the server and then make a redirect to another page. The problem is that before redirecting saga makes second request to the server. I guess there is something wrong with cancelling it. Here is a part of my code:
export function* fetchData() {
...
console.log('fetched');
yield browserHistory.push('/another-page');
}
export function* fetchDataWatcher() {
while (yield take('FETCH_DATA')) {
yield call(fetchData);
}
}
export function* fetchDataRootSaga() {
const fetchWatcher = yield fork(fetchDataWatcher);
yield take(LOCATION_CHANGE);
yield cancel(fetchWatcher);
}
export default [
fetchDataRootSaga
]
So in this example i have two console logs, the second one appears before redirecting. How can i fix it?
And another question. Actually, i have more functions in this file. Should i create "rootSaga" for each of them or i can cancel them all in that fetchDataRootSaga()? I mean is it normal if i cancel sagas this way:
export function* fetchDataRootSaga() {
const watcherOne = yield fork(fetchDataOne);
const watcherTwo = yield fork(fetchDataTwo);
...
yield take(LOCATION_CHANGE);
yield cancel(watcherOne);
yield cancel(watcherTwo);
...
}
Thanks in advance!
P.S. I'm not sure if this code is best practices. It is inspired by this repository
Maybe start by adjusting your loop inside fetchDataWatcher to look a little more like this
export function* fetchDataWatcher() {
while (true) {
yield take('FETCH_DATA');
yield call(fetchData);
}
}
Also you can route better by doing something like this perhaps
import { push } from 'react-router-redux';
import { put } from 'redux-saga/effects';
export function* fetchData() {
...
console.log('fetched');
yield put(push('/another-page'));
}
Overall I would hesitate to put a route change and then altogether separately do a take on it, only if you wish to cancel on all location changes (but I assume that's what you're after :) )
This defeats the purpose of saga, which is to handle potentially long running async requests and returns. You could instead set a state in your redux store like so
export function* fetchData() {
...
console.log('fetched');
yield put(setRedirectState('/another-page'));
}
Then see if the redirect state is set in your container in ComponentWillUpdate and redirect accordingly to something like this
import { push } from 'react-router-redux';
dispatch(push(state.redirecturl))
I haven't tried this, but the experience I have with React-boilerplate, this is what I would try first.