redux saga yield all cancel other effect when one failed - javascript

I have issues with yield all in saga effect, I provide my sample code below
function* fetchData(item) {
try {
const data = yield call(request, url);
yield put(fetchDataSuccess(data));
} catch (error) {
yield put(fetchDataFailure(error));
throw error;
}
}
function* fetchSummary(action) {
try {
yield all(
list.map(item=>
call(fetchData, item)
)
);
} catch (error) {
yield put(
enqueueSnackbar({
message: "Has Error",
options: { variant: "error" }
})
);
}
}
The logic of it is that I want to call multiple requests (some success, and some failed).
Expected: If it has failed request, the error will be caught after yield all but those success requests still continue and it should dispatch action "fetchDataSuccess" after individual success request (Promise.all can do this)
Actual: If it has failed request, the error will be caught after yield all, and then saga immediately cancel all other "fetchData" call.
Can anyone help me to achieve this logic. Thanks in advance.

The "Actual" behavior that you are describing fits with what I am seeing in your code. As soon as any error is thrown, we leave the try block and enter the catch block.
When we yield an array of effects, the generator is blocked until all the effects are resolved or as soon as one is rejected (just like how Promise.all behaves). - docs
If you want each fetch to execute then you would need to put the try/catch inside the .map. You can either map to an array of true/false values or set a value on error. Or if you don't mind having multiple snackbars you could put enqueueSnackbar inside fetchData instead of in fetchSummary.
Here's one way to do it:
// modified to return either true or false
function* fetchData(item) {
try {
const data = yield call(request, item);
yield put(fetchDataSuccess({ item, data }));
return true;
} catch (error) {
yield put(fetchDataFailure({ item, error }));
return false;
}
}
function* fetchSummary(action) {
const results = yield all(
action.payload.list.map((item) => call(fetchData, item))
);
// check if any of the results were false;
const hasError = results.some((res) => !res);
if (hasError) {
yield put(
enqueueSnackbar({
message: "Has Error",
options: { variant: "error" }
})
);
}
}
Code Sandbox Demo

Related

Problem with select in redux-saga. Error: call: argument of type {context, fn} has undefined or null `fn`

After looking through some answers to similar questions here, I just can't get my selector to work. Here's my selector.js:
export const getButtonStatus = state => state.buttonStatus;
(That's the entirety of the file. I don't know if I have to import anything into it. Didn't seem like it from looking at other answers I've seen here.)
and here's what I'm where I'm trying to access the selector in my saga:
import { takeLatest, call, put, select } from "redux-saga/effects";
import { getButtonStatus } from "./selector.js";
...
export function* watcherSaga() {
yield takeLatest("get-tweets", workerSaga);
}
function* workerSaga() {
try {
const buttonStatus = yield select(getButtonStatus);
const response = yield call(getTweets(buttonStatus));
const tweets = response.tweets;
yield put({
type: "tweets-received-async",
tweets: tweets,
nextTweeter: response.nextTweeter
});
} catch (error) {
console.log("error = ", error);
yield put({ type: "error", error });
}
}
...
Here's the error I'm receiving:
Error: call: argument of type {context, fn} has undefined or null `fn`
I'm new to Saga. Can anyone tell me what I'm doing wrong?
The error is not with your selector but with your yield call - it takes the function as an arg followed by the arguments to pass to the function: https://redux-saga.js.org/docs/api/#callfn-args. So it should be:
const response = yield call(getTweets, buttonStatus);
Otherwise looks good!
The Problem
You are probably doing this:
const foo = yield call(bar())
So you don't pass the function itself, but rather the function call.
The Fix
Try to only send the function, not its call.
const foo = yield call(bar)
Notice that we have bar only, not bar().

How to make an async request using redux-saga

I'm trying to make a request to get some user info with redux sagas.
so far I have:
function* getUserDetails() {
const userDetails = axios.get('http://localhost:3004/user').then(response => response)
yield put({ type: 'USER_DATA_RECEIVED', user: userDetails})
}
function* actionWatcher() {
yield takeLatest('GET_USER_DATA', getUserDetails)
}
export default function* rootSaga() {
yield all([
actionWatcher(),
]);
}
but when I log that out user either comes back as undefined or Promise<pending>. so I tried to add in yield call(axios stuff in here)
but that didn't seem to work either
anyone got any ideas either a) how to use call properly? and b) how to pass through a payload with the action?
The correct way to use the call effect in your case would be this:
function* getUserDetails() {
const userDetails = yield call(axios.get, 'http://localhost:3004/user');
yield put({ type: 'USER_DATA_RECEIVED', user: userDetails})
}
The first argument for call is the function you want to call, subsequent arguments are arguments you want to pass to the called function.
Improved Version
Calls to external APIs can always go wrong, so it's a good practice to safeguard against this by wrapping a try/catch block around the Axios call.
In the catch block, you could, for example, dispatch an action that signals an error, which you can use to show an error message to the user.
function* getUserDetails() {
let userDetails;
try {
userDetails = yield call(axios.get, 'http://localhost:3004/user');
} catch (error) {
yield put({ type: 'USER_DATA_ERROR', error });
return;
}
yield put({ type: 'USER_DATA_RECEIVED', user: userDetails})
}

how to handle redux saga error in the wrapper?

This is the Normal Way:
function* saga1() {
try {
// do stuff
} catch (err) {
// handle err
}
}
function* saga2() {
try {
} catch (err) {
}
}
function* wrapper() {
yield [
takeLatest('saga1', saga1),
takeLatest('saga2', saga2),
];
}
This is Expected Way:
function* saga1() {
}
function* saga2() {
}
function* wrapper() {
try {
takeLatest('saga1', saga1),
takeLatest('saga2', saga2),
} catch (err) {
// handle errors
}
}
Is there anyway to achieve above way to handle errors? Using normal way sometimes leading to repeatedly handle same error.
Easiest way for this case is fork as using parallel dynamic effect in saga. Of course, it is not real thread, but way to write sequence of asynchronous operations.
Let see your example in depth. Construction like yield [ takeA(), takeB() ] supposes that you delegate A&B operation to saga workflow with supplied callback. In other words, execution of wrapper is done for this moment, so try/catch is not appropriate.
Is alternative you can fork as parallel dynamic effect for two or more independent saga-processes, and make infinite loop in which of them.
Code:
function* proc1() {
while(true) {
yield call(() => new Promise(resolve => setTimeout(resolve, 1500)));
throw new Error('Err1')
}
}
function* proc2() {
while(true) {
yield call(() => new Promise(resolve => setTimeout(resolve, 2000)));
throw new Error('Err2')
}
}
function* watchLoadRequest() {
try {
yield [call(proc1), call(proc2)]
} catch(err) {
console.log('##ERROR##', err);
}
}
Of course, you should implement custom business login in parallel procedures. If you need a persistent/shared state between them, use object argument with appropriate fields.

How to migrate api call wrapper from a redux thunk to redux saga

I have recently started using redux-saga and I'm really liking it.
I have the following wrapper which I was using for my api calls which would take a promise (my api call), and display a preloader and handle errors.
export const callApi = (promise: Promise<any>, errorMsg: string = 'Api error') => (dispatch: Dispatch) => {
dispatch(setLoading(true));
return promise.then(
(response) => {
dispatch(setLoading(false));
return response.body;
},
(error) => {
dispatch(setLoading(false));
dispatch(apiError(errorMsg, error));
return error;
});
};
I'm unsure how I would replicate behaviour like this in redux saga. I couldnt find any example of doing anything like this?
So far I've come up with
const camelizeKeysPromise = (obj) => Promise.resolve(camelizeKeys(obj));
export function* sagaCallApi(promise: Promise<any>, errorMsg: string = 'Api error') {
yield put(setLoading(true));
try {
const response = yield call(promise);
try {
const result = yield call(camelizeKeysPromise(response.body));
return result;
} catch (e) {
return response.body;
}
} catch (exception) {
yield put(setLoading(false));
yield put(apiError(errorMsg, error));
};
}
Yielding a call to promise will not return the desired response. You can use eventChannel from redux-saga to create a channel that emits the response on success or the error object on failure and then subscribe to the channel in your saga.
const promiseEmitter = promise => {
return eventChannel(emit => {
promise.then(
response => emit({response}),
error => emit({error})
);
});
};
Modify your new saga by replacing the call to the promise with this:
const channel = yield call(promiseEmitter, promise);
const {response, error} = yield take(channel);
if(response){
// handle success
return response;
}else if(error){
// handle failure
yield put(setLoading(false));
yield put(apiError(errorMsg, error));
}
Be aware that there might be syntactical errors in my code as I wrote this without an editor, but you can get the general approach.

testing redux-saga inside if statement and using real values

How do I test a function inside an if statement or try/catch? For instance,
export function* onFetchMessages(channel) {
yield put(requestMessages())
const channel_name = channel.payload
try {
const response = yield call(fetch,'/api/messages/'+channel_name)
if(response.ok){
const res = yield response.json();
const date = moment().format('lll');
yield put(receiveMessages(res,channel.payload,date))
}
} catch (error){
yield put(rejectMessages(error))
}
}
I need to input a real channel name that actually exist in the database for it to return a valid response for the yields that follow to execute, otherwise it will throw an error. In addition, I will get an error message, cannot read property json of undefined, so the yield after that cannot be reached due to this error message.
So my first problem is 'if(response.ok)' but even if I remove it, yield response.json() would return an error and in addition the yield after that wont be executed.
If anyone can show me how to test these, would be much appreciated.
Pass the response object to the previous execution and test conditional, I would do it like this, hope this helps:
export function* onFetchMessages(channel) {
try {
yield put(requestMessages())
const channel_name = channel.payload
const response = yield call(fetch,'/api/messages/'+channel_name)
if(response.ok){
const res = yield response.json();
const date = moment().format('lll');
yield put(receiveMessages(res,channel.payload,date))
}
} catch (error){
yield put(rejectMessages(error))
}
}
describe('onFetchMessages Saga', () => {
let output = null;
const saga = onFetchMessages(channel); //mock channel somewhere...
it('should put request messages', () => {
output = saga.next().value;
let expected = put(requestMessages()); //make sure you import this dependency
expect(output).toEqual(expected);
});
it('should call fetch...blabla', ()=> {
output = saga.next(channel_name).value; //include channel_name so it is avaiable on the next iteration
let expected = call(fetch,'/api/messages/'+channel_name); //do all the mock you ned for this
expect(output).toEqual(expected);
});
/*here comes you answer*/
it('should take response.ok into the if statemenet', ()=> {
//your json yield is out the redux-saga context so I dont assert it
saga.next(response).value; //same as before, mock it with a ok property, so it is available
output = saga.next(res).value; //assert the put effect
let expected = put(receiveMessages(res,channel.payload,date)); //channel should be mock from previous test
expect(output).toEqual(expected);
});
});
Notice your code probably does more stuff I'm not aware of, but this at least should put u in some line to solve your problem.
You might want to use an helper library for that, such as redux-saga-testing.
Disclaimer: I wrote this library to solve that exact same problem
For your specific example, using Jest (but works the same for Mocha), I would do two things:
First, I would separate the API call to a different function
Then I would use redux-saga-testing to test your logic in a synchronous way:
Here is the code:
import sagaHelper from 'redux-saga-testing';
import { call, put } from 'redux-saga/effects';
import { requestMessages, receiveMessages, rejectMessages } from './my-actions';
const api = url => fetch(url).then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error(response.status); // for example
}
});
function* onFetchMessages(channel) {
try {
yield put(requestMessages())
const channel_name = channel.payload
const res = yield call(api, '/api/messages/'+channel_name)
const date = moment().format('lll');
yield put(receiveMessages(res,channel.payload,date))
} catch (error){
yield put(rejectMessages(error))
}
}
describe('When testing a Saga that throws an error', () => {
const it = sagaHelper(onFetchMessages({ type: 'foo', payload: 'chan1'}));
it('should have called the API first, which will throw an exception', result => {
expect(result).toEqual(call(api, '/api/messages/chan1'));
return new Error('Something went wrong');
});
it('and then trigger an error action with the error message', result => {
expect(result).toEqual(put(rejectMessages('Something went wrong')));
});
});
describe('When testing a Saga and it works fine', () => {
const it = sagaHelper(onFetchMessages({ type: 'foo', payload: 'chan2'}));
it('should have called the API first, which will return some data', result => {
expect(result).toEqual(call(api, '/api/messages/chan2'));
return 'some data';
});
it('and then call the success action with the data returned by the API', result => {
expect(result).toEqual(put(receiveMessages('some data', 'chan2', 'some date')));
// you'll have to find a way to mock the date here'
});
});
You'll find plenty of other examples (more complex ones) on the project's GitHub.
Here's a related question: in the redux-saga docs, they have examples where take is listening for multiple actions. Based on this, I wrote an auth saga that looks more or less like this (you may recognize that this is a modified version of an example from the redux-saga docs:
function* mySaga() {
while (true) {
const initialAction = yield take (['AUTH__LOGIN','AUTH__LOGOUT']);
if (initialAction.type === 'AUTH__LOGIN') {
const authTask = yield fork(doLogin);
const action = yield take(['AUTH__LOGOUT', 'AUTH__LOGIN_FAIL']);
if (action.type === 'AUTH__LOGOUT') {
yield cancel(authTask);
yield call (unauthorizeWithRemoteServer)
}
} else {
yield call (unauthorizeWithRemoteServer)
}
}
}
I don't think this is an anti-pattern when dealing with Sagas, and the code certainly runs as expected outside the test environment (Jest). However, I see no way to handle the if statements in this context. How is this supposed to work?

Categories

Resources