Handling errors with redux-toolkit - redux

The information about the error in my case sits deeply in the response, and I'm trying to move my project to redux-toolkit. This is how it used to be:
catch(e) {
let warning
switch (e.response.data.error.message) {
...
}
}
The problem is that redux-toolkit doesn't put that data in the rejected action creator and I have no access to the error message, it puts his message instead of the initial one:
While the original response looks like this:
So how can I retrieve that data?

Per the docs, RTK's createAsyncThunk has default handling for errors - it dispatches a serialized version of the Error instance as action.error.
If you need to customize what goes into the rejected action, it's up to you to catch the initial error yourself, and use rejectWithValue() to decide what goes into the action:
const updateUser = createAsyncThunk(
'users/update',
async (userData, { rejectWithValue }) => {
const { id, ...fields } = userData
try {
const response = await userAPI.updateById(id, fields)
return response.data.user
} catch (err) {
if (!err.response) {
throw err
}
return rejectWithValue(err.response.data)
}
}
)

We use thunkAPI, the second argument in the payloadCreator; containing all of the parameters that are normally passed to a Redux thunk function, as well as additional options: For our example async(obj, {dispatch, getState, rejectWithValue, fulfillWithValue}) is our payloadCreator with the required arguments;
This is an example using fetch api
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
export const getExampleThunk = createAsyncThunk(
'auth/getExampleThunk',
async(obj, {dispatch, getState, rejectWithValue, fulfillWithValue}) => {
try{
const response = await fetch('https://reqrefs.in/api/users/yu');
if (!response.ok) {
return rejectWithValue(response.status)
}
const data = await response.json();
return fulfillWithValue(data)
}catch(error){
throw rejectWithValue(error.message)
}
}
)
Simple example in slice:
const exampleSlice = createSlice({
name: 'example',
initialState: {
httpErr: false,
},
reducers: {
//set your reducers
},
extraReducers: {
[getExampleThunk.pending]: (state, action) => {
//some action here
},
[getExampleThunk.fulfilled]: (state, action) => {
state.httpErr = action.payload;
},
[getExampleThunk.rejected]: (state, action) => {
state.httpErr = action.payload;
}
}
})
Handling Error
Take note:
rejectWithValue - utility (additional option from thunkAPI) that you can return/throw in your action creator to return a rejected response with a defined payload and meta. It will pass whatever value you give it and return it in the payload of the rejected action.

For those that use apisauce (wrapper that uses axios with standardized errors + request/response transforms)
Since apisauce always resolves Promises, you can check !response.ok and handle it with rejectWithValue. (Notice the ! since we want to check if the request is not ok)
export const login = createAsyncThunk(
"auth/login",
async (credentials, { rejectWithValue }) => {
const response = await authAPI.signin(credentials);
if (!response.ok) {
return rejectWithValue(response.data.message);
}
return response.data;
}
);

Related

Infinite loop on Redux saga yield call using Axios with JWT tokens

I have been trying to obtain data using Axios through Redux-saga using Redux-toolkit & react. It appears that intercepting a saga call with a token gets redux-saga in an infinite loop? Or is it because of my watchers?
I have recently been learning how to program so my skills in all areas are not yet great, hope you dont mind the way the code is written as I have been following tutorials mostly.
On handleSubmit from a Header.tsx to dispatch
const handleSubmit = (e) => {
e.preventDefault();
dispatch(getCurrentUser());
};
my rootSaga.tsx includes all watcherSagas notices the dispatch for getCurrentUser()
import { takeLatest } from "redux-saga/effects";
import {
handleLogInUser,
handleGetCurrentUser,
handleSetCurrentUser,
} from "./handlers/user";
import {
logInUser,
getCurrentUser,
setCurrentUser,
} from "../slices/user/userSlice";
export function* watcherSaga() {
yield takeLatest(logInUser.type, handleLogInUser);
yield takeLatest(getCurrentUser.type, handleGetCurrentUser);
yield takeLatest(setCurrentUser.type, handleSetCurrentUser);
}
the watcher calls handleGetCurrentUser for the saga located in user.tsx file in handler folder:
import { call, put } from "redux-saga/effects";
import { setCurrentUser } from "../../slices/user/userSlice";
import { requestLogInUser, requestGetCurrentUser } from "../requests/user";
export function* handleLogInUser(action) {
try {
console.log(action + "in handleLogInUser");
yield call(requestLogInUser(action));
} catch (error) {
console.log(error);
}
}
export function* handleGetCurrentUser(action) {
try {
const response = yield call(requestGetCurrentUser);
const userData = response;
yield put(setCurrentUser({ ...userData }));
} catch (error) {
console.log(error);
}
}
Which then uses yield call to requestGetCurrentUser which fires off the request to the following user.tsx in requests folder
import axiosInstance from "../../../axios/Axios";
export function requestGetCurrentUser() {
return axiosInstance.request({ method: "get", url: "/user/currentUser/" });
}
The response is given back and put in const userData, I consoleLog()'d the handler and discovered the following:
it will reach the handler successfully
go to the yield call
obtain the data successfully
return the data back to the handler
then it restarts the entire yield call again?
It also never makes it back to the userSlice in order to put the data.
axiosInstance in my axios.tsx file which includes the interceptor and gets the access_token and adds it to the header.
import axios from "axios";
const baseURL = "http://127.0.0.1:8000/api/";
const axiosInstance = axios.create({
baseURL: baseURL,
timeout: 5000,
headers: {
Authorization: "Bearer " + localStorage.getItem("access_token"),
"Content-Type": "application/json",
accept: "application/json",
},
});
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async function (error) {
const originalRequest = error.config;
if (typeof error.response === "undefined") {
alert(
"A server/network error occurred. " +
"Looks like CORS might be the problem. " +
"Sorry about this - we will get it fixed shortly."
);
return Promise.reject(error);
}
if (
error.response.status === 401 &&
originalRequest.url === baseURL + "token/refresh/"
) {
window.location.href = "/login/";
return Promise.reject(error);
}
if (
error.response.data.code === "token_not_valid" &&
error.response.status === 401 &&
error.response.statusText === "Unauthorized"
) {
const refreshToken = localStorage.getItem("refresh_token");
if (refreshToken) {
const tokenParts = JSON.parse(atob(refreshToken.split(".")[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
console.log(tokenParts.exp);
if (tokenParts.exp > now) {
return axiosInstance
.post("/token/refresh/", {
refresh: refreshToken,
})
.then((response) => {
localStorage.setItem("access_token", response.data.access);
localStorage.setItem("refresh_token", response.data.refresh);
axiosInstance.defaults.headers["Authorization"] =
"JWT " + response.data.access;
originalRequest.headers["Authorization"] =
"JWT " + response.data.access;
return axiosInstance(originalRequest);
})
.catch((err) => {
console.log(err);
});
} else {
console.log("Refresh token is expired", tokenParts.exp, now);
window.location.href = "/login/";
}
} else {
console.log("Refresh token not available.");
window.location.href = "/login/";
}
}
// specific error handling done elsewhere
return Promise.reject(error);
}
);
export default axiosInstance;
The userSlice.tsx
import { createSlice } from "#reduxjs/toolkit";
const userSlice = createSlice({
name: "user",
initialState: {},
reducers: {
logInUser(state, action) {},
getCurrentUser() {},
setCurrentUser(state, action) {
const userData = action.payload;
console.log(userData + "we are now back in slice");
return { ...state, ...userData };
},
},
});
export const { logInUser, getCurrentUser, setCurrentUser } = userSlice.actions;
export default userSlice.reducer;
I discovered that if I were to remove the authorization token it only fires off once and gets out of the infinite loop since it throws the unauthorised error.
Any suggestions would be greatly appreciated, thanks!
Apologies for getting back so late, I managed to fix it a while ago by pure chance and I dont exactly understand why.
But I believe what fixed it were the following two things:
Changing the useEffect that dispatched the action and ensuring that the handler returned data that the useEffect was expecting to be updated.
In the handler I deconstructed the userData to { userData } which I believe means that the data returned from the axios request is not the entire request but the actual returned data.
my handler
export function* handleGetCurrentUser() {
try {
console.log("in request get user");
const response = yield call(requestGetCurrentUser);
const { data } = response;
yield put(setCurrentUser({ ...data }));
} catch (error) {
console.log(error);
}
}
I forgot to add my useEffect to the post, which created the action.
my useEffect in the App.tsx would dispatch the call when the App was rendered for the first time. However because the returned data did not update what was expected it kept rerendering.
I cant exactly remember what my useEffect was but currently it is the following:
my useEffect in App.tsx
const dispatch = useDispatch();
useEffect(() => {
dispatch(getCurrentUser());
}, [dispatch]);
const user = useSelector((state) => state.user);

Accessing the server response when an error occurs in my saga request

I'm using the library redux-saga-requests for network communication and high level state management. Following the guidance from the documentation, I've set up a global error handler to display a notification in case the server request times out or the user has connection troubles.
However, when I get a server error (500 for example), I can't access the response in my action. The server response is just not there.
Is there a way to access this object while still staying within the confines and rules of redux-saga?
Here's the initial store setup - note that onErrorSaga doesn't give me the network response, so I might as well go with the default behaviour - sending a suffixed _ERROR action that can be caught by my reducer:
function* onErrorSaga(error, action) {
console.log('error?', error)
console.log('action?', action)
// None of these contain the server response,
// just the error message and stack thrown by the client.
yield { error }
}
function* rootSaga(axiosInstance) {
yield createRequestInstance({
driver: {
default: createAxiosDriver(axiosInstance),
},
onError: onErrorSaga,
})
yield watchRequests()
}
const configureStore = initialState => {
const sagaMiddleware = createSagaMiddleware()
const middlewares = [thunkMiddleware, requestsPromiseMiddleware({ auto: true }), sagaMiddleware, trackingMiddleware]
const store = createStore(rootReducer, initialState, composeWithDevTools(applyMiddleware(...middlewares)))
sagaMiddleware.run(rootSaga, axiosInstance)
return store
}
export const configureStoreAsync = () => {
return new Promise((resolve, reject) => {
const store = configureStore()
store
.dispatch(fetchAppInitialization())
.then(result => resolve(store))
.catch(e => reject(store))
})
}
Here's how I'm currently handling errors in my state (which is then displayed by a notification component):
import { CLOSE_NOTIFICATION } from '../actions/actions.notification'
export default (state = [], action) => {
if (action.type.endsWith('_ERROR')) {
return {
type: 'warning',
title: action.error.message,
message: `${action.error.stack.substring(0, 200)}...`,
open: true,
timestamp: new Date(),
}
}
switch (action.type) {
case CLOSE_NOTIFICATION:
return {
...state,
open: false,
}
default:
return state
}
}
Turns out it was way easier than I thought. The action object does in fact contain the server response.
The path to the response is:
action.error.response.data

Proper way to handle errors in redux using redux-promise

I'm trying "redux-promise".
When there's no error in the flow, my code works properly. But, let's say that the API is down or I have a typo in the URL. In those cases, I expect to handle the error in the proper way.
This is the API: https://jsonplaceholder.typicode.com/users
(in the snippet I'm adding random text at the end to produce the 404)
Action creator
export async function fetchUsers() {
const request = await axios
.get('https://jsonplaceholder.typicode.com/userssdfdsfdsf')
.catch(error => console.log('ERROR', error))
return {
type: FETCHING_USERS,
payload: request
};
}
Reducer
export default (state = [], action) => {
switch (action.type) {
case FETCHING_USERS:
return [...state, ...action.payload.data]
default:
return state
}
}
I can see the error logged in the console
ERROR Error: Request failed with status code 404
But, once the action is dispatched its payload is undefined
action {type: "FETCHING_USERS", payload: undefined}
I don't know where is the best place to handle this: action creator, reducer, etc. I shouldn't check if payload is something in the reducer and, if not, return state or do nothing. I want to understand which would be the best approach to handle this.
You may look at source of redux-promise, as it very simple.
Redux-promise expects either promise or action with payload set to some promise. I think you're going to use leter case.
Code may look like (just example, not tested):
export function fetchUsers() {
const request = axios.get('https://jsonplaceholder.typicode.com/userssdfdsfdsf');
return {
type: FETCHING_USERS,
payload: request
};
}
In this case redux-promise will await for resolution of promise returned by axios.get and dispatch your action but payload replaced with promise result. In case of error, redux-promise will catch it and dispatch action with error = true (you may want to handle action.error === true case in reducer)
In the reducer you should check for the existence of the error field in action:
export default function(state = null, action) {
if (action.error) {
//handle
}
switch(action.type) {
This code is for the action.
export const fetchUsers = () => async dispatch => {
try {
const res = await axios.get('https://jsonplaceholder.typicode.com/userssdfdsfdsf');
dispatch({
type: FETCHING_USERS,
payload: res.data
});
} catch (err) {
dispatch({
type: FETCHING_ERROR
});
}
};
In reducer do this.
Define the initial state and use this code.
export default function(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case FETCHING_USERS:
return {
...state,
loading: false,
user: payload,
};
case FETCH_ERROR:
return {
...state,
token: null,
loading: false,
};

How to test asynchonous functions using sinon?

I have a class called PostController, and I trying to test the following function create:
class PostController {
constructor(Post) {
this.Post = Post;
}
async create(req, res) {
try {
this.validFieldRequireds(req);
const post = new this.Post(req.body);
post.user = req.user;
...some validations here
await post.save();
return res.status(201).send(message.success.default);
} catch (err) {
console.error(err.message);
const msg = err.name === 'AppError' ? err.message :
message.error.default;
return res.status(422).send(msg);
}
}
My test class is:
import sinon from 'sinon';
import PostController from '../../../src/controllers/posts';
import Post from '../../../src/models/post';
describe('Controller: Post', async () => {
it.only('should call send with sucess message', () => {
const request = {
user: '56cb91bdc3464f14678934ca',
body: {
type: 'Venda',
tradeFiatMinValue: '1',
... some more attributes here
},
};
const response = {
send: sinon.spy(),
status: sinon.stub(),
};
response.status.withArgs(201).returns(response);
sinon.stub(Post.prototype, 'save');
const postController = new PostController(Post);
return postController.create(request, response).then(() => {
sinon.assert.calledWith(response.send);
});
});
});
But I'm getting the following error:
Error: Timeout of 5000ms exceeded. For async tests and hooks, ensure
"done()"
is called; if returning a Promise, ensure it resolves.
(D:\projeto\mestrado\localbonnum-back-end\test\unit\controllers\post_spec.js)
Why?
Most probably it's because misuse of sinon.stub.
You've
sinon.stub(Post.prototype, 'save');
without telling what this stub will do, so in principle this stub will do nothing (meaning it returns undefined).
IDK, why you don't see other like attempt to await on stub.
Nevertheless, you should properly configuture 'save' stub - for example like this:
const saveStub = sinon.stub(Post.prototype, 'save');
saveStub.resolves({foo: "bar"});

Redux Async actions returns me an error: Actions must be plain objects. Use custom middleware for async actions

I am struggling with the async Redux (thunk). I trully don't understand what I am doing wrong with my async actions and why I get the error : Error: Actions must be plain objects. Use custom middleware for async actions.
export async function startLocalizationFetchingAsync(currentLocalizationState) {
return (dispatch) => {
let payload = {
request: {
sent:true,
}
};
dispatch({
type: "NEW_LOCALIZATION_REQUEST_SENT2",
payload: payload,
});
return axios.get("http://freegeoip.net/json/"+currentLocalizationState.clientIP)
.then(res => {
res = res.data;
var payload = {
country: res.country_name||'',
};
dispatch({
type: "NEW_LOCALIZATION",
payload: payload,
});
})
.catch(function (error) {
console.log("Promise Rejected",error);
dispatch({
type: "NEW_LOCALIZATION_REQUEST_ERROR",
payload: null,
});
});
};
}
while in the index.js router i have the following code
async action({ next, store }) {
// Execute each child route until one of them return the result
const route = await next();
await store.dispatch(startLocalizationFetchingAsync());
this generates me an error:
Error: Actions must be plain objects. Use custom middleware for async actions.
dispatch
webpack:///~/redux/es/createStore.js:153
http://myskyhub.ddns.net:3000/assets/client.js:9796:16
http://myskyhub.ddns.net:3000/assets/vendor.js:46309:16
Object.dispatch
webpack:///~/redux-thunk/lib/index.js:14
Object._callee$
webpack:///src/routes/index.js?a731:35
tryCatch
webpack:///~/regenerator-runtime/runtime.js:65
Generator.invoke
webpack:///~/regenerator-runtime/runtime.js:303
Generator.prototype.(anonymous
webpack:///~/regenerator-runtime/runtime.js:117
http://myskyhub.ddns.net:3000/assets/3.9645f2aeaa83c71f5539.hot-update.js:8:361
while the config store is the following
const middleware = [thunk.withExtraArgument(helpers), thunk.withExtraArgument(AsyncMiddleware)];
let enhancer;
if (__DEV__) {
middleware.push(createLogger());
//middleware.push(AsyncMiddleware());
enhancer = compose(
applyMiddleware(...middleware),
devToolsExtension,
);
} else {
enhancer = applyMiddleware(...middleware);
}
initialState.localization = defaultLocalization; //Location
// See https://github.com/rackt/redux/releases/tag/v3.1.0
const store = createStore(rootReducer, initialState, enhancer);
What I am doing wrong? I don't understand the redux-thunk...

Resources