React and Redux toolkit - reject after promise - reactjs

I'm working on a React Native app. I have a signup screen which has a button, onclick:
const handleClick = (country: string, number: string): void => {
dispatch(registerUser({ country, number }))
.then(function (response) {
console.log("here", response);
navigation.navigate(AuthRoutes.Confirm);
})
.catch(function (e) {
console.log('rejected', e);
});
};
The registerUser function:
export const registerUser = createAsyncThunk(
'user/register',
async ({ country, number }: loginDataType, { rejectWithValue }) => {
try {
const response = await bdzApi.post('/register', { country, number });
return response.data;
} catch (err) {
console.log(err);
return rejectWithValue(err.message);
}
},
);
I have one of my extraReducers that is indeed called, proving that it's effectively rejected.
.addCase(registerUser.rejected, (state, {meta,payload,error }) => {
state.loginState = 'denied';
console.log(`nope : ${JSON.stringify(payload)}`);
})
But the signup component gets processed normally, logging "here" and navigating to the Confirm screen. Why is that?

A thunk created with createAsyncThunk will always resolve but if you want to catch it in the function that dispatches the thunk you have to use unwrapResults.
The thunks generated by createAsyncThunk will always return a resolved promise with either the fulfilled action object or rejected action object inside, as appropriate.
The calling logic may wish to treat these actions as if they were the original promise contents. Redux Toolkit exports an unwrapResult function that can be used to extract the payload of a fulfilled action or to throw either the error or, if available, payload created by rejectWithValue from a rejected action:
import { unwrapResult } from '#reduxjs/toolkit'
// in the component
const onClick = () => {
dispatch(fetchUserById(userId))
.then(unwrapResult)
.then(originalPromiseResult => {})
.catch(rejectedValueOrSerializedError => {})
}

Related

how to pass parameter to generator function in redux saga from jsx?

I have 3 generator function first is "loginUserStart" where the actual request comes then the second one is "LoginUserAsync" which is called in the "loginUserStart" and third is api call function
so I am trying to pass the parameter from my signin component to the loginUserStart function but whenever I console.log(arguments) it is showing nothing
Code:-
Sign-in component
const login = async () => {
arr.userEmail = "sample_email";
arr.userPassword = "sample_password";
console.log(arr);
signinUserStart(arr);
};
const logSubmit = () => {
login();
};
const mapDispatchToProps = (dispatch) => ({
signinUserStart: (data) => dispatch(signinUserStart(data))
});
Action file code
export const signinUserStart = (data) => ({
type: UserActionTypes.Set_SigninUser_Start,
payload: data
})
saga File code
API generator function code
export async function fetchUser(info) {
console.log(info);
const email = 'Admin#gmail.com'; //sample_email
// const passwords = info.userPassword;
const password = 'Admin#123'; //sample_password
try {
const user = await axios.post("http://localhost:5050/sign", {
data: {
email: email,
password: password,
},
});
console.log(user);
return user;
} catch (error) {
console.log(error);
return error;
}
}
LoginUserAsync function
export function* LoginUserAsync(data) {
console.log("in saga");
console.log(data);
try {
let userInfo = yield call(fetchUser, data)
console.log(userInfo);
yield put(setUserId('62b1c5ee515317d42239066a')); //sample_token
yield put(setCurrentUserName(userInfo.data.userName));
} catch (err) {
console.log(err);
}
}
loginUserStart function
export function* loginUserStart(action) {
console.log(action.payload);//not logging anything for showing in console
yield takeLatest(UserActionTypes.Set_SigninUser_Start, LoginUserAsync(action));
}
I can't be sure without seeing more code, but assuming that loginUserStart is either root saga or started from root saga it means there is no action for it to receive.
The main issue I think is this line
yield takeLatest(UserActionTypes.Set_SigninUser_Start, LoginUserAsync(action));
In the second parameter you are calling the generator function which is wrong, instead you should be passing the saga itself (as reference).
So it should look like this:
yield takeLatest(UserActionTypes.Set_SigninUser_Start, LoginUserAsync);
This way, the Redux Saga library will then call LoginUserAsync when Set_SigninUser_Start is dispatched with first param correctly set to the action object.

await of generator completing in redux-saga

I have code in component,
I need to get updated authorizedError value in function, but i get old value authorized error
// login component
const authorizedError = useSelector((state: RootState) => state.user.authorizedError);
const onSignInPress = useCallback(async () => {
await dispatch(userActions.postLoginUser({username: email, password}));
if (authorizedError) {
setNotificationErrors(['Wrong login or password'])
showNotification();
}
}, [authorizedError, validate, email, password]);
// postLoginUserSaga.js
export default function* postLoginUserSaga({
payload,
}: PayloadAction<UserCredentialsPayload>) {
try {
yield put(setSignInError(false));
const {
data: {
payload: { access_token },
status,
},
} = yield transport.post(URLS.postLoginUserURL, payload);
if (status !== "Ok") {
throw new Error(status);
}
yield setItemAsync(ACCESS_TOKEN_KEY, access_token);
yield put(setSignIn(true));
} catch (error) {
console.error("User login failed", error);
yield put(setSignInError(true));
}
}
// sagaRoot file
export default function* userRootSaga() {
yield all([
checkAuthSaga(),
takeEvery(actions.postLoginUser, postLoginUserSaga),
takeEvery(actions.postRegistrationUser, postRegistrationUserSaga),
takeEvery(actions.getProfileData, getProfileDataSaga),
]);
}
Redux actions don't return a promise, you can't use them like this.
If you want to use the promise API you can use the redux-thunk middleware which supports it.
If you want to use sagas you can add a callback action property instead.
// in component callback
dispatch(userActions.postLoginUser({username: email, password, callback: (authorizedError) => {
if (authorizedError) {
setNotificationErrors(['Wrong login or password'])
showNotification();
}
}));
// in saga
try {
...
action.callback();
} catch (err) {
action.callback(err);
}
Although that has its own issues.
Usually you communicate from sagas back to components by changing the redux state, so you can e.g. have a state for redux error, and based on that field show the error message or show different component if the login was succesful.

action.payload in creactAsyncThunk is undefined

I am trying to get user data from api using axios with createAsyncThunk, and want the user data to be stored in state by the fulfilled action dispatched by the createAsyncThunk.
As mentioned in the docs
if the promise resolved successfully, dispatch the fulfilled action with the promise value as action.payload.
But the action.payload in undefined in the fulfilled action creator.
Here is my code.
/// Create Async Thunk
export const fetchUserData = createAsyncThunk(
'user/fetchUserData',
(payload, { dispatch }) => {
axios
.get('/user')
.then(res => {
console.log(res.data);
//Used this as a work around for storing data
dispatch(setUser(res.data));
return res.data;
})
.catch(err => {
console.error(err);
return err;
});
}
);
/// On Fulfilled
const userSlice = createSlice({
...
extraReducers:{
...
[fetchUserData.fulfilled]: (state, action) => {
// Payload is undefined
state.data = action.payload
},
}
}
createAsyncThunk accepts two parameters:
type
payloadCreator
Where payloadCreator is a callback function that should return a promise (containing the result of some asynchronous logic) or a value (synchronously).
So, you can either write:
export const fetchUserData = createAsyncThunk(
'user/fetchUserData',
(payload, { dispatch }) => {
return axios.get('/user'); // Return a promise
}
);
or
export const fetchUserData = createAsyncThunk(
'user/fetchUserData',
async (payload, { dispatch, rejectWithValue }) => {
try {
const response = await axios.get('/user')
return response // Return a value synchronously using Async-await
} catch (err) {
if (!err.response) {
throw err
}
return rejectWithValue(err.response)
}
}
);
An addition to #Ajeet Shah's answer:
According to the documentation a rejected promise must return either
an Error-instance, as in new Error(<your message>),
a plain value, such as a descriptive String,
or a RejectWithValue return by thunkAPI.rejectWithValue()
With the first two options, and I haven't tested the last option, the payload will also by undefined, but an error parameter will be given containing your rejected message.
See this example:
const loginAction = createAsyncThunk(
"user/login",
(payload, { getState }) => {
const { logged_in, currentRequestId, lastRequestId } = getState().login;
// Do not login if user is already logged in
if (logged_in) {
return Promise.reject(new Error(Cause.LoggedIn));
}
// Do not login if there is a pending login request
else if (lastRequestId != null && lastRequestId !== currentRequestId) {
return Promise.reject(new Error(Cause.Concurrent));
}
// May as well try logging in now...
return AccountManager.login(payload.email, payload.password);
}
);

Should I handle errors in my action creators

In the following context how should I handle possible errors:
export async function testAction(data) {
try {
let response = await axios.get(`https://jsonplaceholder.typicode.com/todos/1`);
return {
type: 'TEST_ACTION',
payload: response
}
} catch(err) {
// ???
}
}
// Somewhere in a component:
<Button onClick={ () => dispatch( testAction() ) }>
Test Stuff
</Button>
Or is better to actually dispatch from the component, eg:
refactor action creator:
export async function testAction(data) {
try {
let response = await axios.get(`https://jsonplaceholder.typicode.com/todos/1`);
return response
} catch(err) {
return err
}
}
Somewhere in a component:
const handleTestAction = () => {
testAction().then(r => dispatch( { type: 'TEST_ACTION', payload: r } ) ).catch( // hadnle errors)
}
<Button onClick={ handleTestAction }>
Test Stuff
</Button>
I know the redux style guide recommends using Action Creators for dispatching actions but in this particular case I am calling the action first and then use dispatch. How should I approach it?
You can create another reducer to handle errors.
export async function testAction(data) {
try {
let response = await axios.get(`https://jsonplaceholder.typicode.com/todos/1`);
return {
type: 'TEST_ACTION',
payload: response
}
} catch(err) {
return {
type: 'ERROR',
payload: err
}
}
}
But you cannot do it like above. because the process is asynchronous
You have to use a 'redux-thunk' for that. Once you add it as a middle-ware to your store, you can get the dispatcher in to your action creater, so you can dispatch anything in the test action after you complete.
So your reducer should change to the below one,
export async function testAction(data) {
return (dispatch) => {
try {
let response = await
axios.get(`https://jsonplaceholder.typicode.com/todos/1`);
dispatch({
type: 'TEST_ACTION',
payload: response
})
} catch(err) {
dispatch({
type: 'ERR',
payload: response
})
}
}
}
UPDATE
Once you connect the middleware, you can use dispatch in the action creater,
const store = createStore(
reducers,
{},
applyMiddleware(thunk)
);
You need to only add the thunk to the store just like above.
You can make it more clear by refactor your code like below
export const testAction = () => async (dispatch) => {
try {
let response = await axios.get(`https://jsonplaceholder.typicode.com/todos/1`);
dispatch({
type: 'TEST_ACTION',
payload: response
})
} catch(err) {
dispatch({
type: 'ERR',
payload: response
})
}
}
If your API is going to change in dev and prod modes, You can use below way,
Somewhere in your application,
const axiosInstatnce = axios.create({
baseURL: "https://jsonplaceholder.typicode.com",
headers: {/* you can set any header here */}
});
Now when you create store,
const store = createStore(
reducers,
{},
applyMiddleware(thunk.withExtraArgument(axiosInstatnce))
);
Now you can get the axiosInstance as the third argument of the function you return from the testAction. 2nd argument gives the current state.
export const testAction = () => async (dispatch, state, api) => {
try {
let response = await api.get(`/todos/1`);
dispatch({
type: 'TEST_ACTION',
payload: response
})
} catch(err) {
dispatch({
type: 'ERR',
payload: response
})
}
}
Now in your component,
import {testAction} from '../path/to/actions'
const dispatch = useDispatch()
dispatch(testAction())
If you want to write async code in an action creator, you need to write an async action creator. Regular action creators return an object whereas async action creators return a function instead of an object.
export function testAction(data) {
return async function(dispatch) {
// async code
}
}
Inside the function returned by an async action creator, you have access to dispatch which can be used to dispatch any success action in case of successful response from server and in case of error, you can dispatch an action indicating that an error has occurred.
export function testAction(data) {
return async function (dispatch) {
try {
let response = await axios.get(`https://jsonplaceholder.typicode.com/todos/1`);
dispatch({
type: 'TEST_ACTION',
payload: response
});
} catch(err) {
dispatch({type: 'TEST_ACTION_ERROR', message: 'error occurred'});
}
}
}
You also need to use redux-thunk middleware if you have async action creators in your code. This middleware allows action creators to return a function.
For complete details about how to create async action creators and how to setup redux-thunk middleware to make async creators work, take a look at Async Actions

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

Resources