Handling Errors with Custom Axios and redux-saga - reactjs

I have the following pipeline in my React App
saga.js :
function* handleGetTrack(action: ReturnType<typeof selectTrack>) {
try {
const getTrackResponse = yield httpGetTrack(action.payload)
yield console.log(getTrackResponse)
getTrackResponse.status === 200
? yield put(trackSelected(getTrackResponse.data))
: yield put(selectTrackError('Track Not Found (Get)'))
} catch (err) {
if (err instanceof Error && err.stack) {
yield put(selectTrackError(err.stack))
} else {
yield put(selectTrackError('An unknown error occured.'))
}
}
}
Api.js
export function httpGetTrack(trackId) {
return customAxios.get(`${URL}/tracks/${trackId}`)
}
customAxios.js :
export const customAxios = axios.create({
baseURL: process.env.REACT_APP_API_ENDPOINT,
timeout: 10000,
})
customAxios.interceptors.response.use(
function(response) {
return response
},
function(error) {
const errorResponse = error.response
if (isTokenExpiredError(errorResponse)) {
return resetTokenAndReattemptRequest(error)
}
return error.response
}
)
Like this everything works fine, In fact in my saga.js I can make the console.log(getTrackResponse) print the error well,
However in the Axios documentation it says tu use return Promise.reject(error) instead of return error.response
Why is that ?? Am i doing well or wrong ??

Have you tried using the call effect? For example:
import { call } from 'redux-saga/effects';
...
const getTrackResponse = yield call(httpGetTrack, action.payload);
...
When you define your interceptor you should also follow the Axios docs
function(error) {
const errorResponse = error.response
...
return Promise.reject(error.response);
}

import Api from './path/to/api'
import { call, put } from 'redux-saga/effects'
function fetchProductsApi() {
return Api.fetch('/products')
.then(response => ({ response }))
.catch(error => ({ error }))
}
function* fetchProducts() {
const { response, error } = yield call(fetchProductsApi)
if (response)
yield put({ type: 'PRODUCTS_RECEIVED', products: response })
else
yield put({ type: 'PRODUCTS_REQUEST_FAILED', error })
}
Documentation
Here is an answer for a similar problem

Related

Redux-sagas doesn't recognize function from backend.js file

I have the following redux-sagas:
import { all, call, fork, put, takeEvery } from "redux-saga/effects";
import { catalogs } from 'backend/Catalogs';
import {
ON_FETCH_CATALOGS,
ON_CRUD_POPULATE_LIST,
ON_CRUD_FETCH_NEXT_PAGE,
ON_CRUD_FETCH_PREVIOUS_PAGE,
ON_CRUD_FETCH_PAGE,
ON_CRUD_CREATE,
ON_CRUD_DELETE,
ON_CRUD_REFRESH_PAGE,
ON_CATALOG_POPULATE_OPTIONS,
ON_CRUD_UPDATE,
ON_CRUD_FETCH_LIST,
ON_CRUD_FETCH_RECORD,
GET_TAGS,
ON_FETCH_AUTOMATS,
} from "constants/ActionTypes";
import {
fetchCatalogsSuccess,
showCatalogsMessage,
crudPopulateListSuccess,
crudFetchNextPageSuccess,
crudFetchPreviousPageSuccess,
crudFetchPageSuccess,
crudRefreshPage,
crudRefreshPageSuccess,
catalogPopulateOptionsSuccess,
crudFetchListSuccess,
crudFetchRecordSuccess,
ListTagsSuccess,
fetchAutomatsSuccess,
} from 'actions/Catalogs';
import { ALERT_ERROR ,ALERT_SUCCESS } from 'attractora/AttrAlert';
...
/// fetchAutomats
const fetchAutomatsRequest = async () => {
return await catalogs.fetchAutomats()
.then (automats => automats)
.catch (error => error)
}
function* fetchAutomatsFromRequest ({payload}) {
try {
const automats = yield call(fetchAutomatsRequest);
if (automats.message) {
yield put(showCatalogsMessage(ALERT_ERROR, automats.message));
} else {
yield put(fetchAutomatsSuccess(automats));
}
} catch (error) {
yield put(showCatalogsMessage(ALERT_ERROR, error));
}
}
export function* fetchAutomats() {
yield takeEvery(ON_FETCH_AUTOMATS, fetchAutomatsFromRequest);
}
/// rootSaga
export default function* rootSaga() {
yield all([
fork(fetchCatalogsList),
fork(crudPopulateList),
fork(crudFetchNextPage),
fork(crudFetchPreviousPage),
fork(crudFetchPage),
fork(crudCreateRecord),
fork(crudUpdateRecord),
fork(crudDeleteRecord),
fork(crudSagaRefreshPage),
fork(catalogPopulateOptions),
fork(crudFetchList),
fork(crudFetchRecord),
fork(crudPopulateListTags),
fork(fetchAutomats),
]);
}
And this backend.js file:
export const fetchAutomats = () => {
var requestString = backendServer + 'api/general/automats_for_form/'
axios.get(requestString, getAuthHeader())
.then((Response) => {
return { automats: Response.data, message: null };
})
.catch((Error) => {
return getErrorMessage(Error);
})
}
export const catalogs = {
getCatalogs: getCatalogs,
populateList: populateList,
fetchNextPage: fetchNextPage,
fetchPreviousPage: fetchPreviousPage,
fetchPage: fetchPage,
createRecord: createRecord,
deleteRecord: deleteRecord,
refreshPage: refreshPage,
getList: getList,
updateRecord: updateRecord,
fetchList: fetchList,
fetchRecord: fetchRecord,
populateListTags: populateListTags,
fetchAutomats: fetchAutomats,
searchRepositoryByName,
};
For some reason, line return await catalogs.fetchAutomats() rises the following error: TypeError: Cannot read property 'then' of undefined at fetchAutomatsRequest.
I cannot find where my error lies.
The issue is within the fetchAutomats method, you need to return the promise from the method:
export const fetchAutomats = () => {
var requestString = backendServer + 'api/general/automats_for_form/'
return axios.get(requestString, getAuthHeader())
.then((Response) => {
return { automats: Response.data, message: null };
})
.catch((Error) => {
return getErrorMessage(Error);
})
}

Fetch api data in redux-saga

I want to fetch api data with redux-saga .
Here is how I call my api :
export const getData = () => {
agent
.get(
"https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?CMC_PRO_API_KEY=XXX"
)
.then((res) => {
console.log(res.body.data)
})
.catch((err) => {
console.log(err);
});
};
Here is my redux-saga :
import { takeLatest, put } from "redux-saga/effects";
import { delay } from "redux-saga/effects";
function* loadDataAsync() {
yield delay(500);
yield put({ type: "LOAD_DATA_ASYNC" });
}
export function* watchloadData() {
console.log("Im working buddy");
yield takeLatest("LOAD_DATA", loadDataAsync);
}
The problem Is I don't really know how to fetch data through redux-saga,I tried to do research but none of the information seemed to satisfy me.
Could you please give me some suggestions?
The problem Is I don't really know how to fetch data through redux-saga,
No, the problem is you don't really know function generators in javascript.
Anyways if I were to architect that piece of code I would do something like this:
export function* getData(){
yield agent
.get(
"https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?CMC_PRO_API_KEY=XXX"
)
};
And then:
function* loadDataAsync() {
try {
const { body: { data } } = yield getData();
console.log(data);
yield put({ type: "LOAD_DATA_ASYNC_SUCCESS" });
} catch(err) {
console.log(err);
yield put({ type: "LOAD_DATA_ASYNC_FAILED" });
}
}

Failing to integrate Redux-saga with Redux

I am trying to use Redux-saga for the first time, so I have the following sagas file:
backendAuth.js
import {all, call, fork, put, takeEvery} from "redux-saga/effects";
import {auth} from 'backend/backend';
import {
SIGNIN_USER,
SIGNOUT_USER,
SIGNUP_USER
} from "constants/ActionTypes";
import {showAuthMessage, userSignInSuccess, userSignOutSuccess, userSignUpSuccess} from "actions/Auth";
const createUserWithUsernamePasswordRequest = async (username, password) =>
await auth.createUserWithUsernameAndPassword(username, password)
.then(authUser => {
console.log('authUser: '+authUser);
return authUser;
})
.catch(error => error);
const signInUserWithUsernamePasswordRequest = async (username, password) =>
await auth.signInWithUsernameAndPassword(username, password)
.then(authUser => authUser)
.catch(error => error);
const signOutRequest = async () =>
await auth.signOut()
.then(authUser => authUser)
.catch(error => error);
function* createUserWithUsernamePassword({payload}) {
const {username, email, password} = payload;
try {
const signUpUser = yield call(createUserWithUsernamePasswordRequest, username, email, password);
if (signUpUser.message) {
yield put(showAuthMessage(signUpUser.message));
} else {
localStorage.setItem('user_id', signUpUser.user.uid);
yield put(userSignUpSuccess(signUpUser.user.uid));
}
} catch (error) {
yield put(showAuthMessage(error));
}
}
function* signInUserWithUsernamePassword({payload}) {
const {username, password} = payload;
try {
const signInUser = yield call(signInUserWithUsernamePasswordRequest, username, password);
if (signInUser.message) {
yield put(showAuthMessage(signInUser.message));
} else {
localStorage.setItem('user_id', signInUser.user.uid);
yield put(userSignInSuccess(signInUser.user.uid));
}
} catch (error) {
yield put(showAuthMessage(error));
}
}
function* signOut() {
try {
const signOutUser = yield call(signOutRequest);
if (signOutUser === undefined) {
localStorage.removeItem('user_id');
yield put(userSignOutSuccess(signOutUser));
} else {
yield put(showAuthMessage(signOutUser.message));
}
} catch (error) {
yield put(showAuthMessage(error));
}
}
export function* createUserAccount() {
yield takeEvery(SIGNUP_USER, createUserWithUsernamePassword);
}
export function* signInUser() {
yield takeEvery(SIGNIN_USER, signInUserWithUsernamePassword);
}
export function* signOutUser() {
yield takeEvery(SIGNOUT_USER, signOut);
}
export default function* rootSaga() {
yield all([
fork(signInUser),
fork(createUserAccount),
fork(signOutUser)
]);
}
And this is the file where the asynchronous consult to an api is performed:
backend.js
import axios from 'axios';
const backendServer = 'http://localhost:8000/';
const signInEndpoint = backendServer + 'api/token_auth/';
const signInWithUsernameAndPassword = (username, password) => {
axios.post(backendServer+"api/token_auth/", {
username: username,
password: password
})
.then(Response => {
console.log('Response: '+Response)
return Response;
})
.catch(Error => Error);
}
export const auth = {
signInWithUsernameAndPassword: signInWithUsernameAndPassword
}
The ajax is well executed through axios, and the console.log() in backend.js is reached, but the console.log() in backendAuth is not, and I get the following error in the console:
index.js:1375 Invariant Violation: Objects are not valid as a React child (found: TypeError: Cannot read property 'then' of undefined). If you meant to render a collection of children, use an array instead.
I believe the problem lies in the way I am defining the return of the value of the then in the ajax of backend.js, but I am pretty new to frontend development, so I am not sure about it.
The root cause is a simple fix - you are using an explicit block for the body of your signInWithUsernameAndPassword arrow function, so you need to use an explicit return. Since you have no return statement that function returns undefined which is cascading to cause the overall issue.
The quick fix is just to update the function to:
const signInWithUsernameAndPassword = (username, password) => {
return axios.post(backendServer+"api/token_auth/", {
username: username,
password: password
})
.then(Response => {
console.log('Response: '+Response)
return Response;
})
.catch(Error => Error);
}
Now to understand the final error you're seeing due at the moment: since signInWithUsernameAndPassword returns undefined, you eventually enter the catch block of signInWithUsernamePassword in backendAuth.js due to the type error of calling undefined.then in signInUserWithUsernamePasswordRequest. The error in that catch block is an Error instance, not a plain string. I'm assume you're then displaying the error that is passed to showAuthError somewhere in a react component tree, but an Error instance is not something that is valid to be rendered. You could try calling toString() on it first to display future errors instead of causing a rendering error.

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

how to async/await redux-thunk actions?

action.js
export function getLoginStatus() {
return async(dispatch) => {
let token = await getOAuthToken();
let success = await verifyToken(token);
if (success == true) {
dispatch(loginStatus(success));
} else {
console.log("Success: False");
console.log("Token mismatch");
}
return success;
}
}
component.js
componentDidMount() {
this.props.dispatch(splashAction.getLoginStatus())
.then((success) => {
if (success == true) {
Actions.counter()
} else {
console.log("Login not successfull");
}
});
}
However, when I write component.js code with async/await like below I get this error:
Possible Unhandled Promise Rejection (id: 0): undefined is not a function (evaluating 'this.props.dispatch(splashAction.getLoginStatus())')
component.js
async componentDidMount() {
let success = await this.props.dispatch(splashAction.getLoginStatus());
if (success == true) {
Actions.counter()
} else {
console.log("Login not successfull");
}
}
How do I await a getLoginStatus() and then execute the rest of the statements?
Everything works quite well when using .then(). I doubt something is missing in my async/await implementation. trying to figure that out.
The Promise approach
export default function createUser(params) {
const request = axios.post('http://www...', params);
return (dispatch) => {
function onSuccess(success) {
dispatch({ type: CREATE_USER, payload: success });
return success;
}
function onError(error) {
dispatch({ type: ERROR_GENERATED, error });
return error;
}
request.then(success => onSuccess, error => onError);
};
}
The async/await approach
export default function createUser(params) {
return async dispatch => {
function onSuccess(success) {
dispatch({ type: CREATE_USER, payload: success });
return success;
}
function onError(error) {
dispatch({ type: ERROR_GENERATED, error });
return error;
}
try {
const success = await axios.post('http://www...', params);
return onSuccess(success);
} catch (error) {
return onError(error);
}
}
}
Referenced from the Medium post explaining Redux with async/await: https://medium.com/#kkomaz/react-to-async-await-553c43f243e2
Remixing Aspen's answer.
import axios from 'axios'
import * as types from './types'
export function fetchUsers () {
return async dispatch => {
try {
const users = await axios
.get(`https://jsonplaceholder.typicode.com/users`)
.then(res => res.data)
dispatch({
type: types.FETCH_USERS,
payload: users,
})
} catch (err) {
dispatch({
type: types.UPDATE_ERRORS,
payload: [
{
code: 735,
message: err.message,
},
],
})
}
}
}
import * as types from '../actions/types'
const initialErrorsState = []
export default (state = initialErrorsState, { type, payload }) => {
switch (type) {
case types.UPDATE_ERRORS:
return payload.map(error => {
return {
code: error.code,
message: error.message,
}
})
default:
return state
}
}
This will allow you to specify an array of errors unique to an action.
Another remix for async await redux/thunk. I just find this a bit more maintainable and readable when coding a Thunk (a function that wraps an expression to delay its evaluation ~ redux-thunk )
actions.js
import axios from 'axios'
export const FETCHING_DATA = 'FETCHING_DATA'
export const SET_SOME_DATA = 'SET_SOME_DATA'
export const myAction = url => {
return dispatch => {
dispatch({
type: FETCHING_DATA,
fetching: true
})
getSomeAsyncData(dispatch, url)
}
}
async function getSomeAsyncData(dispatch, url) {
try {
const data = await axios.get(url).then(res => res.data)
dispatch({
type: SET_SOME_DATA,
data: data
})
} catch (err) {
dispatch({
type: SET_SOME_DATA,
data: null
})
}
dispatch({
type: FETCHING_DATA,
fetching: false
})
}
reducers.js
import { FETCHING_DATA, SET_SOME_DATA } from './actions'
export const fetching = (state = null, action) => {
switch (action.type) {
case FETCHING_DATA:
return action.fetching
default:
return state
}
}
export const data = (state = null, action) => {
switch (action.type) {
case SET_SOME_DATA:
return action.data
default:
return state
}
}
Possible Unhandled Promise Rejection
Seems like you're missing the .catch(error => {}); on your promise. Try this:
componentDidMount() {
this.props.dispatch(splashAction.getLoginStatus())
.then((success) => {
if (success == true) {
Actions.counter()
} else {
console.log("Login not successfull");
}
})
.catch(err => {
console.error(err.getMessage());
}) ;
}
use dispatch(this.props.splashAction.getLoginStatus()) instead this.props.dispatch(splashAction.getLoginStatus())

Resources