react redux, thunk fulfilled event happens prematurely - reactjs

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

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

How To Make A Post Request Using Redux Thunk With Redux Toolkit

I have been using Redux-Toolkit more than the React-Redux. I came across situations where I had to make GET requests, So I recently started using Redux-Thunk (before this I used useEffect but, as it's not a standard way to handle async functions, when using redux. I learned about middleware).
Here is the code of my Thunk function nad extraReducer which handles the GET request
export const fetchData = createAsyncThunk("type/getData", async () => {
try {
const response = await axios({url});
return response.data;
} catch (error) {
console.log(error.response);
}
});
export const extraReducers = {
[fetchData.pending]: (state) => {
state.loading = true;
},
[fetchData.fulfilled]: (state, action) => {
state.loading = false;
state.products = action.payload;
},
[fetchData.rejected]: (state) => {
state.loading = false;
state.error = true;
},
};
In fetchData function my returned response.data is being used in extraReducers as payload so I can set the state easily. But, now the scenario is I have make a post request and I don't know how will I send the data to my Thunk function.
First you create the action of posting data and send the data:
export const postData = createAsyncThunk(
"type/postData",
async (data) => {
try {
const response = await axios.post("https://reqres.in/api/users", data);
// If you want to get something back
return response.data;
} catch (err) {
console.error(err)
}
}
);
Then in a place where you want to send that data you just dispatch this action with the data argument that you want to send:
const handlePost = () => {
// whatever you want to send
const data = .........
dispatch(postData(data));
}
If you want, you can also modify your extraReducers for this action.

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

React Redux - How to make a double dispatch

I'm fetch some data from my API and it correctly works. But when a double dispatch on the same page the API doesn't work anymore. It's better code to explain it:
Server:
router.get("/", (req, res) => {
let sql = "SELECT * FROM design_categories";
let query = connection.query(sql, (err, results) => {
if (err) throw err;
res.header("Access-Control-Allow-Origin", "*");
res.send(results);
});
});
router.get("/", (req, res) => {
let sql = "SELECT * FROM food_categories";
let query = connection.query(sql, (err, results) => {
if (err) throw err;
res.header("Access-Control-Allow-Origin", "*");
res.send(results);
});
});
They work.
action.js
export const fetchDesignCat = () => {
setLoading()
return async dispatch => {
const response = await axios
.get("http://localhost:5000/api/designcategories")
.then(results => results.data)
try {
await dispatch({ type: FETCH_DESIGN_CAT, payload: response })
} catch (error) {
console.log("await error", error)
}
}
}
export const fetchFoodCat = () => {
setLoading()
return async dispatch => {
const response = await axios
.get("http://localhost:5000/api/foodcategories")
.then(results => results.data)
try {
await dispatch({ type: FETCH_FOOD_CAT, payload: response })
} catch (error) {
console.log("await error", error)
}
}
}
Both of them work perfectly.
reducer.js
const initalState = {
db: [],
loading: true,
designcat: [],
foodcat: [],
}
export default (state = initalState, action) => {
switch (action.type) {
// different cases
case FETCH_DESIGN_CAT:
return {
designcat: action.payload,
loading: false,
}
case FETCH_FOOD_CAT:
return {
food: action.payload,
loading: false,
}
}
The reducer updates the states perfectly.
Page settings.js
const Settings = ({ designcat, foodcat, loading }) => {
const dispatch = useDispatch()
// ... code
useEffect(() => {
dispatch(fetchDesignCat()) // imported action
dispatch(fetchFoodCat()) // imported action
// eslint-disable-next-line
}, [])
// ... code that renders
const mapStateToProps = state => ({
designcat: state.appDb.designcat,
foodcat: state.appDb.foodcat,
loading: state.appDb.loading,
})
export default connect(mapStateToProps, { fetchDesignCat, fetchFoodCat })(
Settings
)
Now there's the problem. If I use just one dispatch it's fine I get one or the other. But if I use the both of them look like the if the second overrides the first. This sounds strange to me.
From my ReduxDevTools
For sure I'm mistaking somewhere. Any idea?
Thanks!
Your reducer does not merge the existing state with the new state, which is why each of the actions just replace the previous state. You'll want to copy over the other properties of the state and only replace the ones your specific action should replace. Here I'm using object spread to do a shallow copy of the previous state:
export default (state = initalState, action) => {
switch (action.type) {
case FETCH_DESIGN_CAT:
return {
...state, // <----
designcat: action.payload,
loading: false,
}
case FETCH_FOOD_CAT:
return {
...state, // <----
food: action.payload,
loading: false,
}
}
}
Since the code is abbreviated, I'm assuming you're handling the default case correctly.
As an additional note, since you're using connect with the Settings component, you don't need to useDispatch and can just use the already connected action creators provided via props by connect:
const Settings = ({
designcat,
foodcat,
loading,
fetchDesignCat,
fetchFoodCat,
}) => {
// ... code
useEffect(() => {
fetchDesignCat();
fetchFoodCat();
}, [fetchDesignCat, fetchFoodCat]);
// ... code that renders
};
There's also a race condition in the code which may or may not be a problem to you. Since you start both FETCH_DESIGN_CAT and FETCH_FOOD_CAT at the same time and both of them set loading: false after finishing, when the first of them finishes, loading will be false but the other action will still be loading its data. If this case is known and handled in code (i.e., you don't trust that both items will be present in the state if loading is false) that's fine as well.
The solution to that would be either to combine the fetching of both of these categories into one thunk, or create separate sub-reducers for them with their own loading state properties. Or of course, you could manually set and unset loading.

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