redux-toolkit fetch many data in createAsyncThunk - reactjs

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

Related

How to use a thunk dispatch to create a standard promise that returns `success` or `error`

I have a toast package that receives a standard promise as an argument and does something upon success or error:
toast.promise(
updateNotePromise,
{
loading: 'Saving...',
success: (data: any) => 'Note saved!',
error: (err) => err.toString()
}
);
This is the promise I pass to the toast, but it returns a <PayloadAction> because it calls a thunk:
const updateNotePromise = await dispatch(
updateNoteInFirestore({ note: noteInput, noteDocId: noteProp.docId })
);
How can I return success or error from this dispatch thunk operation?
I thought of processing the returned <PayloadAction> by wrapping the thunk. This would be my naive approach:
const updateNotePromise = async(): Promise<{success: boolean | error: any}> => {
try {
await dispatch(updateNoteInFirestore({ note: noteInput, noteDocId: noteProp.docId
return success }))
}
catch {
(error)=> return error}
Am I on the right track?
Edit: here's the thunk code:
export const updateNoteInFirestore = createAsyncThunk(
'updateNoteInFirestore',
async (
{ note, noteDocId }: { note: string; noteDocId?: string },
{ getState, dispatch }
) => {
const poolState = (getState() as RootState).customerPool.pool;
const userState = (getState() as RootState).user;
const time = Timestamp.now();
const path = noteDocId ? noteDocId : undefined;
const message = note;
if (poolState?.docID) {
await notesService.updateNote(
{
pool: poolState.docID,
customer: userState?.user?.uid ?? 'Undefined Customer',
//we do not update dateFirstCreated
...(path ? { dateLastUpdated: time } : { dateFirstCreated: time }),
dateLastUpdated: time,
message: message,
editHistory: [],
seenByAdmin: false
},
path
);
dispatch(fetchNotesByCustomerId(userState?.user?.uid));
return { error: false };
}
return { error: true };
}
);
If you want to return an error with createAsyncThunk you can use rejectWithValue
const fetchUserById = createAsyncThunk(
'users/fetchById',
async (userId, { rejectWithValue }) => {
const response = await fetch(`https://example.com/api/stuff`)
if (response.status === 404)
return rejectWithValue(new Error("Impossible to do stuff"));
return response.json()
}
)
I think for your use case, it's better to use a promise-based function followed by a dispatch reducer action rather than an asyncThunk.
asyncThunks return value can only be consumed by builders that are defined within slice as far as I know.
You need to break your problem into three steps:
Creating a wrapper promiseFunction as needed by your toast.
Creating a promise helper function where you must be able to supply the variables poolState and userState as these variables were accessed through getState() in your async thunk but that isn't possible in your promiseHelperFunction If you define promiseHelperFunction within your functional component you could use useAppSelector to access those states. I have added the comment for the same in the promiseHelperFunction.
Now once you're done with this you can now consume promiseFunction in your toast.
You might need to import fetchNotesByCustomerId that you're using in your asyncThunk as it may not be accessible to the component where you're writing the toast implementation.
Here's the code for same:
const updateNotePromise = async () => {
return updatePromiseHelperFunction({
note: noteInput,
noteDocId: noteProp.docId,
});
};
const updatePromiseHelperFunction = async ({
note,
noteDocId,
}: {
note: string;
noteDocId?: string;
}) => {
/*
// Before the Promise you must ensure you're able to access these variables:
const poolState = useAppSelector(state => state.customerPool.pool);
const userState = useAppSelector(state => state.user);
*/
const time = Timestamp.now();
const path = noteDocId ? noteDocId : undefined;
if (poolState?.docID) {
await notesService.updateNote(
{
pool: poolState.docID,
customer: userState?.user?.uid ?? 'Undefined Customer',
//we do not update dateFirstCreated
...(path ? { dateLastUpdated: time } : { dateFirstCreated: time }),
dateLastUpdated: time,
message: note,
editHistory: [],
seenByAdmin: false,
},
path
);
dispatch(fetchNotesByCustomerId(userState?.user?.uid));
return { error: false };
}
return { error: true };
};

Redux gives error "Error: Actions must be plain objects. Use custom middleware for async actions."

I'm very new to redux. I tried my best to resolve this error in redux but it still gives me an error.
The error is message is:
Error: Actions must be plain objects. Use custom middleware for async
actions.
Please help me resolve this.
The error points to the line after return. Here is my code:
const mapDispatchToProps = (dispatch) => {
return{
fetchState: (e) => dispatch(fetchState(e)),
fetchCapital: (e) => dispatch(fetchCapital(e))
}
}
And fetchState is defined in my action.jsx
export const fetchState = (e) =>{
console.log(e.target)
return (dispatch) => {
console.log("hey")
axios.get('http://localhost:8080/StatesByCountry/'+ e.target.value)
.then(response => {
const stateData = response.data || '';
dispatch(changeState(stateData));
})
.catch( err => {
console.log(err);
});
}
}
You have to use async and await :
export const fetchState = async (e) =>{
await axios.get('http://localhost:8080/StatesByCountry/'+ e.target.value)

react redux, thunk fulfilled event happens prematurely

I have used a similar Thunk and slice in other areas of my project and all work as expected but this particular one does not work, the difference with this Thunk is it contains an axios call that waits for the result so that it can be chained the next axios call.
After logging i think the issue is that the Thunk is firing 'fulfilled' after the first axios call and not waiting for the full function to complete, this one has me stumped on how to fix this issue.
export const getPlanAPI = createAsyncThunk('dataStorePlans/plan', async () => {
const response = await axios.get('/api/routes')
let promises = [];
const routeData = [];
// I think the Thunk is firing 'fulfilled' at this point.
try {
for (let i = 0; i < response.data.length; i++) {
promises.push(axios.get('/api/routedetail?planid=' + response.data[i].id + '&jobcount=' + response.data[i].jobs))
}
} catch (error) {
console.log(error);
}
Promise.all(promises).then(function (results) {
results.forEach(function (response) {
routeData.push(response.data[0]);
})
return routeData
});
});
export const planSlice = createSlice({
name: 'dataStorePlans/plan',
initialState: {
planList: [],
status: ''
},
reducers: {
getPlanState: (state) => {
return state
}
},
extraReducers: {
[getPlanAPI.pending]: (state, action) => {
state.status = 'Loading';
},
[getPlanAPI.fulfilled]: (state, action) => {
state.status = 'Success';
state.planList = action.payload
},
[getPlanAPI.rejected]: (state, action) => {
state.status = 'failed';
action.error = action.error.message;
}
}
});
The main problem is that your Function will not wait for the Promises to be finished. It also did never return the routeData.
You also clutter your function with lots of iterations and pushing stuff around. It is easy to forget where you have to return things. I know async is the new hot stuff, but a promise chain works better here. Even if I keep your structure the function becomes way easier to read if you use map at the right places.
export const getPlanAPI = createAsyncThunk('dataStorePlans/plan', async () => {
const response = await axios.get('/api/routes')
let promises = [];
try {
promises = response.data.map(({id, jobs}) =>
axios.get(`/api/routedetail?planid=${ id }&jobcount=${ jobs }`)
)
} catch (error) {
console.log(error);
}
return Promise
.all(promises)
.then(results =>
results.map(response => response.data[0])
)
});

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

How to make dispatch with redux-thunk with axios

First, apologize for my english.
I'm trying to make a request with redux-thunk.... I dont understan it well.
My idea is make a request using axios but method return undefined before return value of request.
I dont know if I'm passing dispatch well.
Can you help me please? What am I doing wrong???
This is how use dispatch in my component:
....
const mapDispatchToProps = dispatch => {
return {
createCustomersGeoJSON: () => dispatch(createCustomersGeoJSON()),
getAdvicesData: hierarchy => dispatch(getAdvicesData(hierarchy)),
getSocialNetworkData: () => dispatch(getSocialNetworkData()),
dispatch,
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(injectIntl(CustomersWidget));
In actions I do this:
export const getSocialNetworkData = () => {
return dispatch => {
dispatch({
type: GET_SOCIAL_NETWORK_DATA,
payload: fetchSocialNetworkData(),
});
};
};
And this is the code of fetchSocialNetworkData function:
axios
.get(`http://localhost:5000/socialRoute`)
.then(data => {
let response = Object.assign({}, data);
if (
response &&
response.data &&
response.data.tweets &&
Array.isArray(response.data.tweets)
) {
console.log("data.tweets: ", response.data.tweets);
return response.data.tweets;
}
return [];
})
.catch(error => {
console.log("Error gettin data from socialRoute: ", error);
});
It's because you think you're returning the response but what you're actually returning is nothing because you've handled the result of the promise in a .then chain.
You have two options:
Return a promise and resolve it in the .then:
function fetchSocialNetworkData() {
return new Promise((resolve) => {
axios
.get(`http://localhost:5000/socialRoute`)
.then(data => {
let response = Object.assign({}, data);
if (
response &&
response.data &&
response.data.tweets &&
Array.isArray(response.data.tweets)
) {
console.log("data.tweets: ", response.data.tweets);
resolve(response.data.tweets);
}
resolve([]);
})
})
}
OR
Use async/await (the modern way)
async function fetchSocialNetworkData() {
const data = await axios.get(`http://localhost:5000/socialRoute`);
let response = Object.assign({}, data);
if (
response &&
response.data &&
response.data.tweets &&
Array.isArray(response.data.tweets)
) {
console.log("data.tweets: ", response.data.tweets);
return response.data.tweets;
}
return [];
}
Both of these are the same thing under the hood. IE they're both different ways of writing a promise.
Now. in your thunk, you're still just calling that function, which means you're going to get the unresolved promise rather than the result of that promise. So the thunk becomes:
export const getSocialNetworkData = () => {
return async (dispatch) => {
dispatch({
type: GET_SOCIAL_NETWORK_DATA,
payload: await fetchSocialNetworkData(),
});
};
};
The thing to take away from this is that you can get far without understanding promises but that lack of understanding will always be a ceiling for your JS skills.

Resources