react-axios-middleware promise handling - reactjs

I am working on a React application which uses redux-axios-middleware to do XHR requests. The way it is setup is with a client in the index.js like so:
const client = axios.create({ //all axios can be used, shown in axios documentation
baseURL:'https://api.vktest.xxx/api',
responseType: 'json',
validateStatus: function (status) {
return status >= 200 && status < 300
}
});
const axiosMiddle = axiosMiddleware(client)
export const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk, axiosMiddle)))
From my component I am then calling my action like so:
store.dispatch(loginAction(credentials))
This is the action:
import { LOGIN } from './types'
export function loginAction(credentials) {
return dispatch => {
return dispatch(doLogin(credentials) ).then(
response => {
dispatch({type: 'LOGIN_SUCCESS', response})
},
error => {
dispatch({type: 'LOGIN_FAIL', error})
throw error
}
)
}
}
function doLogin(credentials) {
return {
type: LOGIN,
payload: {
request: {
url: '/login/',
method: 'POST',
data: {
username: credentials.username,
password: credentials.password
}
}
}
}
}
It dispatches the doLogin function which is captured by axios-middleware to do the XHR call. After the call the axios promise contains either the type LOGIN_SUCCESS or LOGIN_FAIL. Axios automatically dispatches these types and the reducers can act on them. So far so good.
However (and I am trying to be as clear as possible), when the axios request fails, the axios promise is rejected and the LOGIN_FAIL type is dispatched. Since that promise is resolved the .then(..).catch(..) block is alway's calling the .then. I cannot seem to handle the axios error with doing something ugly in the .then block which handles the dispatch callback and not the axios callback.
To clearify the following is happening:
store.dispatch -> action -> doLogin -> axios -> reducer -> back to action dispatch.
I want to handle XHR errors between the axios and reducer step but when I do:
import { LOGIN } from './types'
export function loginAction(credentials) {
return dispatch => {
return dispatch(doLogin(credentials).then(
response => {
dispatch({type: 'LOGIN_SUCCESS', response})
},
error => {
dispatch({type: 'LOGIN_FAIL', error})
throw error
}
))
}
}
function doLogin(credentials) {
return {
type: LOGIN,
payload: {
request: {
url: '/login/',
method: 'POST',
data: {
username: credentials.username,
password: credentials.password
}
}
}
}
}
I get the following error:
Uncaught TypeError: doLogin(...).then is not a function
at authentication.js:6
The reason I want to fix this is because the current setup dispatches the following:
As seen in the image both the LOGIN_FAIL (axios) and the LOGIN_SUCCESS (dispatch) are being called. I want to be able to handle the axios error myself since I want to be able to for example give the user feedback of a failed login attempt. Somehow I am not able to figure out how.
Can someone help me?

I think I know what you mean.
You can change that behavior by setting returnRejectedPromiseOnError in middleware config.
Note that promise is returned only to chain actions. If you set this option you will have to always apply catch callback or you will be getting unhandled rejection when calling Promise errors. And this might be causing some issues when handled in components, because these callbacks may be fired when component is already unmounted.
Im my code I try to avoid then as much as possible to avoid these side effects but when I have to I do it like:
fetchSomething.then(action => {
if(action.type.endsWith('SUCCESS')) {
....
}
})

Related

React query mutation: getting the response from the server with onError callback when the API call fails

I am working on a React JS project. In my project, I am using React query, https://react-query.tanstack.com/docs/guides/mutations. I am using mutation to make the post request to the server. But I am trying the get the response returns from the server when the API call fails with the onError call back.
This is my code.
let [ createItem ] = useMutation(payload => createItem(payload), {
onSuccess: (response) => {
},
onError: (error) => {
// here I am trying to get the response. In axios, we can do something like error.data.server_error_code
},
onMutate: () => {
}
})
As you can see in the comment, I am trying to read a field returned from the server within the onError callback. How can I do that?
let [ createItem ] = useMutation(payload => createItem(payload), {
onSuccess: (response) => {
},
onError: (error) => {
console.log(error.response.data);
console.log(error.response.status);
},
onMutate: () => {
}
})
It's not entirely clear when just doing console.log(error) inside onError, but error.response should be available.
It should work as it is. Make sure that your HTTP client (probably, Axios) is configured to throw an error. For example:
import axios from 'axios'
import { useMutation } from 'react-query'
import { BASE_URL } from 'constants/api'
const client = axios.create({
baseURL: BASE_URL,
})
const request = (options) => {
const onSuccess = (response) => response
const onError = (error) => {
// Throwing an error here
throw error
}
return client(options).then(onSuccess).catch(onError)
}
const { mutate } = useMutation(
async (data) =>
await request({
url: '/someUrl',
method: 'post',
data
}),
{ onError: (e) => console.log(e) }
)
And of course, it's better to store your Axios settings within a separate file, and then just import the 'request' variable where mutations are using.
If you are using fetch, you have to know that fetch does not throw any error unless is a network problem (as read here)
My solution was just to change to axios (which throws error when 400 or 500), but if you still need to use fetch, you need to find a way to make it throw errors instead.
I think the issue with NOT having an error.response in the callback depends on how the API is failing. If you look at the react-query documentation it shows that most HTTP libs like axios will throw if there is a non 2xx response. However it's up to the underlying API function how it handles that.
For example axios https://axios-http.com/docs/handling_errors will return the response object if there is a response from the server. They will return the request if the call has timed out and return just a message if the previous two don't fit the error
axios.get('/user/12345')
.catch(function (error) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.log(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.log('Error', error.message);
}
console.log(error.config);
});
However, if you're using the Fetch API you have handle this yourself. Taken straight from react-query's docs: https://react-query.tanstack.com/guides/query-functions#usage-with-fetch-and-other-clients-that-do-not-throw-by-default
useQuery(['todos', todoId], async () => {
const response = await fetch('/todos/' + todoId)
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
})

React, Redux and Axios - trying to make API call

It's my first experience with React, Redux and I am totally lost. The problem is my action :
import axios from 'axios';
import { FETCH_MOVIE } from '../constants/actionTypes';
const API_KEY = <API_KEY>;
const ROOT_URL = `<API_URL>`;
export function fetchMovies(pop){
const url = `${ROOT_URL}?api_key=${API_KEY}&sort_by=${pop}`;
axios.get(url)
.then(function (response) {
console.log("response is",response)
})
.catch(function (error) {
console.log(error);
});
return{
type: FETCH_MOVIE,
payload: response.data
};
}
On Console.log it seems just fine - I can see the response has the data I need. But when I am trying to send response.data to payload it returns the error - response is not defined. What am I doing wrong?
P.s. I also tried to create const result = [] and than result = [...response.data]. The error was - SyntaxError: "result" is read-only
The const error is because, result being a variable that changes over the course of the execution, you must use 'let' and not 'const'.
Now, for the fix, response is not defined comes from the last return. A good approach would be to, instead of returning the action on this function fetchMovies, you should dispatch a new action, e.g dispatch(fetchMoviesSuccess(payload)) instead of "console.log("response is",response)", which will dispatch an action that will trigger the reducer, and , in turn, update the state of the app.
You are performing async request using axios. You should dispatch your action using redux-thunk. Installation is easy, read more about thunk here.
Then your action should look like this:
export function fetchMovies(pop) {
return dispatch => {
const url = `${ROOT_URL}?api_key=${API_KEY}&sort_by=${pop}`;
axios.get(url)
.then(function (response) {
console.log("response is",response);
dispatch({
type: FETCH_MOVIE,
payload: response.data
});
})
.catch(function (error) {
console.log(error);
// You can dispatch here error
// Example
dispatch({
type: FETCH_MOVIE_FAILED,
payload: error
});
});
}
}
The issue with your code is that by the time you return, response is still undefined because this code run synchronously till the return statement.
As you can see response is defined in console.log("response is",response)
So this is where you need to do your actual magic return but in another way.
You can use redux-thunk to do these thing because this is redux async. but as I feel you are a beginner from the code I have seen, Just use the simpler way and read redux-thunk or redux-promise. if you feel your project needs this then go one.
//try to make the caller pass this.props.dispatch as param
export function fetchMovies(dispatch, pop){
const url = `${ROOT_URL}?api_key=${API_KEY}&sort_by=${pop}`;
axios.get(url)
.then(function (response) {
// only here is response define so use dispatch to triger another action (fetched data with response)
dispatch({
type: FETCH_MOVIE,
payload: response.data
})
})
.catch(function (error) {
//if you had a loader state, you call also toggle that here with erro status
console.log(error);
});
}
//now on the caller (onClick for instance) do this instead
fetchMovies(this.props.dispatch, pop)
As you can see from #loelsonk answer down. if you use redux-thunk then you won't need to pass dispatch from the caller redux-thunk for you. But also notice how you would return and anonymous arrow function which accept dispatch as a parameter.
You can use redux promise middleware. I have used this in my new project. It is very simple and keeps our code and state manageable.
For every async action dispatch, it dispatches
$action_type_PENDING immediately after our action dispatch , $action_type_FULFILLED if api call success, $action_type_REJECTED if api call failure
See documentation- https://github.com/pburtchaell/redux-promise-middleware
Example from my project-
your action is
export function getQuestions() {
return {
type: types.GET_QUESTIONS,
payload: axios.get('http://localhost:3001/questions')
};
}
reducer is
const initialState = {
isLoading: false,
questions: []
};
const questions = (state = initialState.questions, action) => {
switch(action.type) {
case types.GET_QUESTIONS_FULFILLED:
return [...action.payload.data];
default: return state;
}
};
For displaying loader while api call we can use following reducer
const isLoading = (state = initialState.isLoading, action) => {
switch(action.type) {
case (action.type.match(/_PENDING/) || {}).input:
return true;
case (action.type.match(/_FULFILLED/) || {}).input:
return false;
default: return state;
}
};
Comment me if you need any more details on above stuff.

Very slow response from action creator React/Redux

I am getting super slow response times (upwards 10 seconds) for a function to be called in my action creator.
export function acceptContract(id) {
return function(dispatch) {
const config = { headers: { authorization: localStorage.getItem('etherToken') } };
const data = { data: id };
axios.put('/pending-contracts/accept',
data,
config
).then( response => {
console.log(response);
getPendingContracts();
})
.catch( response => {
// If the get doesn't work, boot the user out to index.
console.log(response);
});
}
}
I update one of the values of contracts in my DB, and I want redux to then dispatch the new list for the user to show the update on the UI.
Not sure why the getPendingContract() invocation takes so long. I get the response from my backend almost immediately.
export function getPendingContracts() {
return function(dispatch) {
axios.get('/pending-contracts', {
headers: { authorization: localStorage.getItem('etherToken') }
})
.then( response => {
console.log('in getPendingContracts')
return dispatch({
type: PENDING_CONTRACTS_LIST,
payload: response.data.message
});
})
.catch( response => {
// If the get doesn't work, boot the user out to index.
console.log(response);
});
}
}
The issue might be related to how you are calling getPendingContracts from acceptContract. You are just calling the function directly, without dispatching it. As far as i can tell all that would do is return you a function that never gets invoked, not sure how you get a response at all. Change the call to this:
then( response => {
console.log(response);
dispatch(getPendingContracts());
})

Dispatch async redux action from non-react component with thunk middleware

I am building an react / redux webapp where I am using a service to make all my API calls. Whenever the API returns 401 - Unauthorized I want to dispatch a logout action to my redux store.
The problem is now that my api-service is no react component, so I cannot get a reference to dispatch or actions.
What I did first was exporting the store and calling dispatch manually, but as I read here How to dispatch a Redux action with a timeout? that seems to be a bad practice because it requires the store to be a singleton, which makes testing hard and rendering on the server impossible because we need different stores for each user.
I am already using react-thunk (https://github.com/gaearon/redux-thunk) but I dont see how I can injectdispatch` into non-react components.
What do I need to do? Or is it generally a bad practice to dispatch actions outside from react components?
This is what my api.services.ts looks like right now:
... other imports
// !!!!!-> I want to get rid of this import
import {store} from '../';
export const fetchWithAuth = (url: string, method: TMethod = 'GET', data: any = null): Promise<TResponseData> => {
let promise = new Promise((resolve, reject) => {
const headers = {
"Content-Type": "application/json",
"Authorization": getFromStorage('auth_token')
};
const options = {
body: data ? JSON.stringify(data) : null,
method,
headers
};
fetch(url, options).then((response) => {
const statusAsString = response.status.toString();
if (statusAsString.substr(0, 1) !== '2') {
if (statusAsString === '401') {
// !!!!!-> here I need to dispatch the logout action
store.dispatch(UserActions.logout());
}
reject();
} else {
saveToStorage('auth_token', response.headers.get('X-TOKEN'));
resolve({
data: response.body,
headers: response.headers
});
}
})
});
return promise;
};
Thanks!
If you are using redux-thunk, you can return a function from an action creator, which has dispatch has argument:
const doSomeStuff = dispatch => {
fetch(…)
.then(res => res.json())
.then(json => dispatch({
type: 'dostuffsuccess',
payload: { json }
}))
.catch(err => dispatch({
type: 'dostufferr',
payload: { err }
}))
}
Another option is to use middleware for remote stuff. This works the way, that middle can test the type of an action and then transform it into on or multiple others. have a look here, it is similar, even if is basically about animations, the answer ends with some explanation about how to use middleware for remote requests.
maybe you can try to use middleware to catch the error and dispatch the logout action,
but in that case, the problem is you have to dispatch error in action creator which need to check the log status
api: throw the error
if (statusAsString === '401') {
// !!!!!-> here I need to dispatch the logout action
throw new Error('401')
}
action creator: catch error from api, and dispatch error action
fetchSometing(ur)
.then(...)
.catch(err => dispatch({
type: fetchSometingError,
err: err
})
middleware: catch the error with 401 message, and dispatch logout action
const authMiddleware = (store) => (next) => (action) => {
if (action.error.message === '401') {
store.dispatch(UserActions.logout())
}
}
You should have your api call be completely independent from redux. It should return a promise (like it currently does), resolve in the happy case and reject with a parameter that tells the status. Something like
if (statusAsString === '401') {
reject({ logout: true })
}
reject({ logout: false });
Then in your action creator code you would do:
function fetchWithAuthAction(url, method, data) {
return function (dispatch) {
return fetchWithAuth(url, method, data).then(
({ data, headers }) => dispatch(fetchedData(data, headers)),
({ logout }) => {
if(logout) {
dispatch(UserActions.logout());
} else {
dispatch(fetchedDataFailed());
}
);
};
}
Edit:
If you don't want to write the error handling code everywhere, you could create a helper:
function logoutOnError(promise, dispatch) {
return promise.catch(({ logout }) => {
if(logout) {
dispatch(UserActions.logout());
}
})
}
Then you could just use it in your action creators:
function fetchUsers() {
return function (dispatch) {
return logoutOnError(fetchWithAuth("/users", "GET"), dispatch).then(...)
}
}
You can also use axios (interceptors) or apisauce (monitors) and intercept all calls before they goes to their handlers and at that point use the
// this conditional depends on how the interceptor works on each api.
// In apisauce you use response.status
if (response.status === '401') {
store.dispatch(UserActions.logout())
}

Is there a way to set global axios config for error response codes

I'm using axios in my react/redux application and when I get errors like 401, 404, etc I currently have to deal with them for each action function when I make the calls to axios. I have a axios_config.js where I've wrapped the axios calls with some common idioms. For example:
// need to move this to app config
const BASE_URL = 'http://localhost:8080/api/';
function config() {
return {
headers: {'X-Token-Auth': localStorage.getItem('token')}
}
}
export function fetchData(url) {
return axios.get(`${BASE_URL}${url}`, config());
};
Where I'm struggling are the common errors like 401, 404, etc. Currently, I'm doing this:
export function fetchBrands() {
return function(dispatch) {
dispatch({type:FETCHING_BRANDS});
fetchData('brands')
.then(response => {
dispatch({
type: FETCH_BRANDS_SUCCESS,
payload: response
});
})
.catch(err => {
// deal with errors
});
}
}
But in the catch block, I don't want to have to deal with 401, 404 etc every single time. So I need to be able to deal with those on a more global scale but still have the ability to handle specific errors to the request like server side validation errors for example.
You can use response interceptors as documents in axios documentation.
axios.interceptors.response.use(undefined, function (error) {
if(error.response.status === 401) {
ipcRenderer.send('response-unauthenticated');
return Promise.reject(error);
}
});
other thread with same discussion
You can try to write a function that accepts a function and returns the function with a catch attached. You can even pass an optional secondary argument to execute local catch logic.
This could then be moved to a single file and you can always modify it there.
export function fetchBrand(id) {
return function (dispatch) {
wrapCatch(
fetchData(`brands/${id}`)
.then(response => {
dispatch({
type: FETCH_BRAND_SUCCESS,
payload: response
});
}),
function (err) {
// deal with errors
}
);
}
}
export function wrapCatch(f, localErrors) {
return f.catch(err => {
// deal with errors
localErrors();
});
}
Hope this helps.

Resources