action.payload in creactAsyncThunk is undefined - reactjs

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

Related

redux-toolkit fetch many data in createAsyncThunk

I need function in redux-toolkit to fetch all data from others slices.
I have this code:
export const getAllData = createAsyncThunk(
'fetchRoot/getAllData',
async (_, { dispatch, rejectWithValue }) => {
const promises = [dispatch(getUsers()), dispatch(getSettings()), dispatch(getClients())];
Promise.all(promises)
.then((res: any) => {
// for (const promise of res) {
// console.log('SSS', promise);
// if (promise.meta.rejectedWithValue) {
// return rejectWithValue(promise.payload);
// }
}
})
.catch((err) => {
console.log(err);
});
}
);
My question: if one of slice fetch function (example: getUsers()) is rejected, how to reject promise.all?
getUsers() function and extraReducers:
export const getUsers = createAsyncThunk('users/getUsers', async (_, { rejectWithValue }) => {
try {
const res = await agent.Users.getAll();
return await res.data;
} catch (err) {
return rejectWithValue(err);
}
});
extraReducers: (builder) => {
builder
// GetUsers lifecycle ===================================
.addCase(getUsers.pending, (state) => {
state.apiState.loading = true;
state.apiState.error = null;
})
.addCase(getUsers.fulfilled, (state, { payload }) => {
state.apiState.loading = false;
state.data = payload;
})
.addCase(getUsers.rejected, (state, { payload }) => {
state.apiState.loading = false;
state.apiState.error = payload;
})
You have it basically right. Once the Promise.all(promises) has resolved you will have an array containing the resolved value of each of your individual thunks.
The individual promises will always resolve and will never reject. They will resolve to either a fulfilled action or a rejected action. In some cases, it will make sense to use the unwrap() property which causes rejected actions to throw errors. But looking at the .meta property will work too.
You can check your action with the isRejected or isRejectedWithValue functions which serve as type guards, that way you won't have any TypeScript errors when accessing properties like action.meta.rejectedWithValue.
The hard part here is trying to return rejectWithValue() from inside a loop. I would recommend unwrapping to throw an error instead.
import { createAsyncThunk, unwrapResult } from "#reduxjs/toolkit";
export const getAllData = createAsyncThunk(
"fetchRoot/getAllData",
async (_, { dispatch }) => {
const promises = [dispatch(getUsers()), dispatch(getSettings()), dispatch(getClients())];
const actions = await Promise.all(promises);
return actions.map(unwrapResult);
}
);
Note that there is no reason to try/catch in your getUsers thunk if you are going to rejectWithValue with the entire caught error object. Just let the error be thrown.
export const getUsers = createAsyncThunk('users/getUsers', async () => {
const res = await agent.Users.getAll();
return res.data;
});

It seems to ignore the Async / Await

On a React page, I have a method called goOut. This method calls upon a Redux action > Node controller > Redux reducer. I can confirm that the correct data values are returned inside the Redux action, the controller method, and the reducer. However, nonetheless, at point 1 below inside the goOut method, it returns undefined.
What am I doing wrong / how could it return undefined if the the reducer is returning the correct values? It is as if the await inside the goOut method is not working...
React page:
import { go_payment } from "../../appRedux/actions/paymentAction";
<button onClick={this.goOut}>
Button
</button>
async goOut(ev) {
try {
const data = { user: parseInt(this.state.userId, 10) };
let result = await this.props.go_payment({data});
console.log(result);
// 1. RETURNS UNDEFINED. As if it tries to execute this line before it has finished the previous line.
{
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators(
{go_payment}, dispatch
);
};
Redux Action:
export const go_payment = (data) => {
let token = getAuthToken();
return (dispatch) => {
axios
.post(`${url}/goController`, data, { headers: { Authorization: `${token}` } })
.then((res) => {
if (res.status === 200) {
// console.log confirms correct data for res.data
return dispatch({ type: GO_SUCCESS, payload: res.data });
})
}
}
Node controller method:
Returns the correct data in json format.
Reducer:
export default function paymentReducer(state = initial_state, action) {
switch (action.type) {
case GO_SUCCESS:
// console.log confirms action.payload contains the correct data
return { ...state, goData: action.payload, couponData: "" };
}
}

How to make the API call to wait until we get required data?

I am using React Native and Redux. In initial state of Redux emailExists is set to null:
const INITIAL_STATE = {
// ...
emailExists: null,
};
when registering user. First, I check if user already exists by sending a request to server and if user already exists, I show a toast.
const registerUser = (values, actions) => {
checkEmail(values.userEmail); // takes time to get result of `emailExists`
if (emailExists) { // `emailExists` is now `null` couldn't wait for response
toastRef.current.show("Email already exists!");
return;
}
}
checkEmail code look like this:
function checkEmail(data) {
return (dispatch) => {
return api_request
.post("register/check/email", { email: data })
.then((res) => {
dispatch(emailExists(res.data.exists));
dispatch(authError("Email already exists"));
})
.catch((err) => {
console.log(`err`, err);
});
};
}
aftering dispatching dispatch(emailExists(res.data.exists));, the emailExists will be either true or false, but the problem is that since request takes time to get data from server, at first load of application emailExists is always set to null. Which means below condition will always be false in first load:
if (emailExists) {
toastRef.current.show("Email already exists!");
return;
}
function emailExists(payload){
return {
type: userConstants.EMAIL_EXISTS,
emailExists: payload
}
}
How do I resolve this issue?
THANK YOU
You can modify functions like this to get the expected result.
function checkEmail(data,callback) {
return (dispatch) => {
return api_request
.post("register/check/email", { email: data })
.then((res) => {
// dispatch(emailExists(res.data.exists)); // You don't need this redux approach now because it will take time and will give you the same error
dispatch(authError("Email already exists"));
callback && callback(res.data.exists) // I am assuming here you got the response that email alreay exists and its value is true.
})
.catch((err) => {
console.log(`err`, err);
});
};
}
Now get the callback when the API request is done and you get the response.
const registerUser = (values, actions) => {
const callback = (isEmailExists) => {
//Here you will get the value of the checkemail API (true/false)
//Now do any action in this block of functions
if (isEmailExists) {
toastRef.current.show("Email already exists!");
return;
}
}
checkEmail(values.userEmail,callback);
}
Mantu's answer is one solution. I would prefer using async/await for this use case. dispatch() returns whatever the function returned by the action creator returns. So in this case you are returning a promise, meaning you can await your dispatch(checkEmail(values.userEmail)) call.
You will need to return whether the email exists from the promise, otherwise even if you weait for your checkEmail action to complete, the emailExists will not be up to date when you access it (since using useSelector will have to rerender the component to reflect the updates in the store).
const registerUser = async (values, actions) => {
const emailExists = await checkEmail(values.userEmail);
if (emailExists) {
toastRef.current.show("Email already exists!");
return;
}
}
function checkEmail(data) {
return (dispatch) => {
return api_request
.post("register/check/email", { email: data })
.then((res) => {
dispatch(emailExists(res.data.exists));
dispatch(authError("Email already exists"));
return res.data.exists;
})
.catch((err) => {
console.log(`err`, err);
});
};
}
If you don't want to return the property from the promise, you will need to get the value from the store synchronously via store.getState():
const registerUser = async (values, actions) => {
await checkEmail(values.userEmail);
const emailExists = selectEmailExists(store.getState());
if (emailExists) {
toastRef.current.show("Email already exists!");
return;
}
}

React and Redux toolkit - reject after promise

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

How to call promise function in reducer?

I am call a promise function in my reducer, so I call it like this:
cart(state, action) {
switch(action.type) {
//...
case LOG_IN:
return getCartProducts(true)
}
}
getCartProducts(isLogin) {
// ....
return cartApi.list({
customerId: "",
items: items
}).then(data => {
return cartReduce(undefined, receiveCartProducts(data.items, false))
})
}
so this just returns a promise, not the object I want to
You should make async actions for these as described in the Redux docs.
import fetch from 'isomorphic-fetch';
// One action to tell your app you're fetching data
export const GET_CART_PRODUCTS_REQUEST = 'GET_CART_PRODUCTS_REQUEST'
function getCartProductsRequest(customerId) {
return {
type: GET_CART_PRODUCTS_REQUEST,
customerId
};
}
// One action to signify success
export const GET_CART_PRODUCTS_SUCCESS = 'GET_CART_PRODUCTS_SUCCESS'
function getCartProductsSuccess(cartProducts) {
return {
type: GET_CART_PRODUCTS_SUCCESS,
cartProducts
};
};
// One action to signify an error
export const GET_CART_PRODUCTS_ERROR = 'GET_CART_PRODUCTS_ERROR'
function getCartProductsSuccess(error) {
return {
type: GET_CART_PRODUCTS_ERROR,
error
};
};
// This is the async action that fires one action when it starts
// And then one of two actions when it finishes
export function fetchCartProducts(customerId) {
return dispatch => {
// Dispatch action that tells your app you're going to get data
// This is where you'll set any 'isLoading' state in your store
dispatch(getCartProductsRequest(customerId))
return fetch('https://api.website.com/customers/' + customerId + '/cartProducts')
.then(response => response.json())
.then(
// Fire success action on success
cartProducts => dispatch(getCartProductsSuccess(cartProducts)),
// Fire error action on error
error => dispatch(getCartProductsError(error))
);
};
};
}

Resources