How to deal with errors using all effect in redux-saga - reactjs

I'm using redux-saga to start multiple requests concurrently as described in the redux-saga docs. The all effect has an all or nothing semantics, similar to Promise.all.
Only if all effects succeed, yield all([...]) succeeds. However, I am doing several requests from which I expect some of them to fail and some of them to succeed. I would like to start all of them in parallel and consume the responses from those requests that have succeeded.
Therefore, I tried to wrap the request into a Promise that always resolves no matter whether the request was successful or not:
// watcher saga
export function* watchMultipleRequests() {
while(true) {
const {ids} = yield take('VIDEOS_REQUEST');
yield fork(doMultipleRequests, ids);
}
}
// worker saga
export function* doMultipleRequests(ids) {
const requests = ids.map(id => {
// api.buildVideoRequest returns a promise once it is invoked
const wrapper = ignoreErrors(api.buildVideoRequest, id);
return call(wrapper);
});
try {
const responses = yield all(requests);
yield put({type: 'VIDEOS_SUCCESS', responses});
} catch (error) {
// should not happen because we are always resolving the promise
console.log(error);
}
};
export function ignoreErrors(fn, ...args) {
return function* () {
yield new Promise(function (resolve) {
return fn(...args)
.then(response => {
console.log('success = ', response);
resolve(response);
})
.catch(response => {
console.log('error = ', response);
resolve(response);
});
});
}
}
I would like to handle the error cases in the reducer. However, if I fire n requests, the responses array contains n times undefined. Has anyone a clue on why this is not working?

The issue is that the ignoreErros function is a generator function.
Implementing it like this:
export function ignoreErrors(fn, ...args) {
return () => {
const ignoreErrorCallback = (response) => response;
return fn(...args).then(ignoreErrorCallback, ignoreErrorCallback);
};
}
is sufficient.

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

How to make await work with redux Saga in React?

The await does not seem to work with Redux saga. I need to wait for my API call to finish and then execute the remaining code. What happens now is that AFTER CALL gets printed before the RESPONSE which means await does not seem to work at all. I'm using async calls but not sure what needs to be done extra from the redux saga side?
async componentWillMount() {
console.log("BEFORE CALL")
await this.props.getUserCredit()
console.log("AFTER CALL")
}
mapDispatchToProps = (dispatch) => {
return {
getUserCredit: () => dispatch(getUserCredit()),
}
};
connect(null, mapDispatchToProps)(MyComponent);
Action
export const getUserCredit = () => {
return {
type: GET_USER_CREDIT,
};
};
Redux Saga
const getUserCreditRequest = async () => {
const response = await Api.get(getCreditUrl)
console.log("REPONSE!!!")
console.log(response)
return response
}
function* getUserCredits() {
try {
const response = yield call(getUserCreditRequest);
if (response.status === okStatus) {
yield put({
userCredit: response.data.userCredit
}
));
}
} catch (error) {}
}
export function* getUserCredit() {
yield takeLatest(GET_USER_CREDIT, getUserCredits);
}
export default function* rootSaga() {
yield all([fork(getUserCredit)]);
}
Normally, init / fetching takes place during componentDidMount and don't use async or await inside components. Let the saga middleware do its thing via yield.
// In your component
componentDidMount() { // not async
this.props.getUserCredit(); // dispatch `GET_USER_CREDIT` action
}
mapDispatchToProps = (dispatch) => {
return {
getUserCredit: () => dispatch(getUserCredit()),
}
};
connect(null, mapDispatchToProps)(YourComponent);
You shouldn't be using async/await pattern. As redux-saga handles it by the yield keyword. By the time call is resolved you will have the value available in response.
in actions.js, you should have an action that will carry your data to your reducer:
export function getUserCredits(userCredit) {
return {
type: types.GET_USER_CREDIT_SUCCESS,
payload: userCredit
};
}
Your saga should handle the API call like so:
function* getUserCredits() {
try {
const response = yield axios.get(getCreditUrl); <--- This should work
// No need for if here, the saga will catch an error if the previous call failed
yield put(actions.getUserCredits(response.data.userCredit));
} catch (error) {
console.log(error);
}
}
EDIT: example of using axios with redux-saga

Waiting for call to finish and then dispatch action in saga

I want to make call to server and then use that data for dispatch of other action.
export function* function1(actions) {
console.log('inside');
try {
console.log('getting past orders list');
const url = `/api/getOrders`;
let reqsData = {
order_id: actions.payload.order_id
};
const data = yield call(request, { url, method: 'POST', data:reqsData })
console.log(data);
console.log('///////////////////////////////////');
if (!data.error) {
console.log(data)
yield put({ type: 'nowThis', payload: actions.payload.data });
} else {
console.log('---------------------------------')
console.log('got some error');
}
} catch (error) {
console.log(error)
}
}
But It is not running code next to line
const data = yield call(request, { url, method: 'POST', data:reqsData })
I have similar code before which is running properly + i checked the network and i am getting response 200 for this line.
I have used fork in place of call but it run my code next to that line before the call is complete.
yield call takes function and arguments. Write a method to make a service call. U can use axios npm package (axios.get('../url',params:{params})) and call that function in yield call.
yield call(methodToCallApi(),params,to,method). also, it is better if you keep services calls in a seperate file and just call those methods in saga, instead of defining directly in saga.
It seems your request method is not returning properly. Wrap that in a Promise:
request() {
return new Promise(resolve => {
myApiCall().then(response => {
resolve(response);
}).catch(e => {
reject(e);
});
});
}
and then in your saga, you can yield as normal:
const data = yield call(request, { url, method: 'POST', data:reqsData })

how to setstate after saga async request

I'm using redux-saga in my project.
I used redux-thunk before, so i can't setState ends of some async request. like
this.props.thunkAsync()
.then(){
this.setState({ '' });
}
Since thunk returns promise, i could use 'then'.
But i can't do this with saga, because saga doesn't return promise.
So i did it in componentWillReceiveProps by checking flag props (like REQUEST_SUCCESS,REQUEST_WAITING...) has been changed.
I think it's not good way to solve this problem.
So.. My question is how can i do some works when async request ends in redux-saga!
But i can't do this with saga, because saga doesn't return promise
Redux-saga is slightly different from thunk since it is process manager, not simple middleware: thunk performs reaction only on fired actions, but saga has its own "process" (Formally callback tick domain) and can manipulate with actions by effects.
Usual way to perform async actions with redux-saga is splitting original actions to ACTION_REQUEST, ACTION_SUCCESS and ACTION_FAILURE variants. Then reducer accepts only SUCCESS/FAILURE actions, and maybe REQUEST for optimistic updates.
In that case, your saga process can be like following
function* actionNameSaga(action) {
try {
const info = yield call(fetch, { params: action.params }
yield put('ACTION_NAME_SUCCESS', info)
} catch(err) {
yield put('ACTION_NAME_FAILURE', err)
}
function* rootSaga() {
yield takeEvery('ACTION_NAME', actionNameSaga)
}
Keep in mind that yield operation itself is not about promise waiting - it just delegates async waiting to saga process manager.
Every api call you make is processed as an async request but handled using a generator function in a saga.
So, After a successful api call, you can do the following possible things.
Make another api call like
function* onLogin(action) {
try {
const { userName, password } = action;
const response = yield call(LoginService.login, userName, password);
yield put(LoginService.loginSuccess(response.user.id));
const branchDetails = yield call(ProfileService.fetchBranchDetails, response.user.user_type_id);
yield put(ProfileActions.fetchBranchDetailsSuccess(branchDetails));
} catch (error) {
yield put(ProfileActions.fetchUserDetailsError(error));
}
}
Pass a Callback after successfull api
onLoginClick() {
const { userName, password } = this.state;
this.props.login(userName, password, this.onLoginSuccess);
}
onLoginSuccess(userDetails) {
this.setState({ userDetails });
}
function *onLogin(action) {
try {
const { userName, password, onLoginSuccess } = action;
const response = yield call(LoginService.login, userName, password);
if (onLoginSuccess) {
onLoginSuccess(response);
}
yield put(LoginService.loginSuccess(response.user.id));
const branchDetails = yield call(ProfileService.fetchBranchDetails,
response.user.user_type_id);
yield put(ProfileActions.fetchBranchDetailsSuccess(branchDetails));
} catch (error) {
yield put(ProfileActions.fetchUserDetailsError(error));
}
}
Update Reducer State and get from props by mapStateToProps
yield put(LoginService.loginSuccess(response.user.id));
#connect(
state => ({
usedDetails: state.user.get('usedDetails'),
})
)
static getDerivedStateFromProps(nextProps, prevState) {
const { usedDetails } = nextProps;
return {
usedDetails
}
}
I was stuck with the same problem...
My solution was wrapping the dispatch in a promise and call the resolve and reject in a saga function...
I created a hook to wrap the dispatch. You can see my example here:
https://github.com/ricardocanelas/redux-saga-promise-example
I hope that can help somebody.
You can do it this way. From component props you call the saga method and pass the function you want to execute after success or failure, like below
export function* login({ payload }) {
const url = 'localhost://login.json';
try {
const response = yield call(App_Service, { url, method: 'GET' });
if (response.result === 'ok' && response.data.body) {
yield put(fetchDataActionCreators.getLoginSuccess(response.data.body));
//function passed as param from component, pass true if success
payload.callback(true);
}
} catch (e) {
//function passed as param from component, pass false if failure
payload.callback(false);
console.log(e);
}
}
export function* watchLogin() {
while (true) {
const action = yield take(LOGIN);
yield* login(action);
}
}
export default function* () {
yield all([
fork(watchLogin)
]);
}
In component call call setState method in the function you pass to saga as param
login() {
// store
this.props.getServiceDetails({
callback:(success) => this.onLoginSuccess(success)
})
}
onLoginSuccess = (success) => {
this.setState({
login:success
})
alert('login '+success)
}

Resources