Preventing a setState from running until network request is complete - reactjs

I have the following function, inside of a Context file in my React app:
const fetchAll = (userId) => {
try {
fetchDetails(userId)
// To be clear... There's multiple functions here, i.e:
// fetchContact(userId)
} catch (err) {
console.log(err)
}
setPending(false)
}
I've removed some of the functions - but the main premise of the function is to combine multiple promises together, and until these are complete, display a 'pending' component.
This pending component is displayed if the 'pending' state is set to true:
const [pending, setPending] = useState(true)
However, at the moment, what is happening is that the try is attempted, but the setPending is executed at the same time.
I thought one way around this would be to utilise a 'finally' call at the end of the my try / catch, but that still executes at the same time. Like this:
const fetchAll = (userId) => {
try {
fetchDetails(userId)
} catch (err) {
console.log(err)
} finally {
setPending(false)
}
}
I don't want any of my functions to be run asynchronously: I want them all to execute at the same time to prevent a waterfall effect of multiple network requests at once.
For reference, my individual 'fetch' functions call an endpoint and set state data based upon the response:
const fetchDetails = (userId) => {
axios.post("/api/fetch/fetchDetails", {
id: userId
})
.then((response) => {
console.log(response.data)
setName(response.data.name)
setPreviewPhoto(response.data.profile_picture_url)
setPhotoName(response.data.profile_picture_name)
setPhotoFile(response.data.profile_picture_url)
})
}
Does anyone have any suggestions as to how I could make this work?

Let's assume you have 2 API calls: fetchAll('123') and fetchAll('321');
In order to wait for all of your requests and then update your state, you should use Promise.all like this:
Promise.all([fetchAll('123'), fetchAll('321')]).then((responses) => {
setPending(false)
}

fetchDetails returning a promise, you need to use async/await
const fetchAll = async (userId) => {
try {
await fetchDetails(userId)
} catch (err) {
console.log(err)
} finally {
setPending(false)
}
}

You can have multiple async calls using Promise.all() or Promise.allSettled() depending on your use case.
setPending(true)
try {
await Promise.all([() => fetchAll(1), () => fetchAll(2)])
} finally {
setPending(false)
}
This will wait for all calls to complete (or one to fail)

Related

Refactoring useEffect to only require one database call

At the moment, I have a component which completes some backend calls to decide when to start displaying the UI.
It's structured like this:
useEffect(() => {
getData()
})
const getData = async () => {
await verifyUser()
await fetchData()
}
The purpose here, is that verifyUser() is supposed to run first, and in the response to verifyUser(), a user id is provided by the backend.
const verifyUser = async () => {
if (!localStorage.getItem('auth')) {
return
}
if (localStorage.getItem('auth')) {
await axios.post("/api/checkAuth", {
token: JSON.parse(localStorage.getItem('auth'))
})
.then((response) => {
return setUserId(response.data.user_id)
})
.catch((err) => {
console.log(err)
localStorage.removeItem('auth')
})
}
}
As a result of this, the fetchData() function is supposed to wait until the verifyUser() function has stopped resolving, so it can use the user id in the database query.
However, at the moment it...
Calls once, without the user id
Then calls again, with the user id (and therefore resolves successfully)
Here's the function for reference:
const fetchData = async () => {
console.log("Fetch data called.")
console.log(userId)
await axios.post("/api/fetch/fetchDetails", {
user_id: userId
})
.then((response) => {
// Sets user details in here...
return response
})
.then(() => {
return setFetching(false)
})
.catch((err) => {
console.log(err)
})
}
What I'm trying to achieve here is to essentially remove any concurrency and just run the functions sequentially. I'm not 100% sure what the best practice here would be, so some feedback would be appreciated!
Your useEffect is missing a dependency array argument:
useEffect(() => {
getData()
})
should be:
useEffect(() => {
getData()
}, [])
Without that argument, useEffect will run once each time your component renders. With that argument, it will only run once, when the component is first mounted (ie. after the first render).
If you needed it to depend on another variable (eg. user.id isn't defined on load, but is later on) you could put that variable in the dependency array, ie.
useEffect(() => {
if (!user.id) return;
getData()
}, [user.id])
This version would run once when the component is mounted, then again if the user.id changes (eg. if it goes from null to an actual number).
In React, the useEffect hook accepts two arguments - the first one is a function (this is the "effect"), and the second one is a dependency array. The simplest useEffect hook looks like this:
useEffect(() => {
}, [])
The above hook has no dependency (because the array is empty), and runs only when the component initially mounts, and then goes silent.
If you don't pass in a dependency array as the second argument, as #machineghost said, the hook will run the "effect" function every time your component re-renders.
Now to your specific problem. You want to run fetchData after verifyUser has resolved its Promise, so you'd add the outcome of verifyUser as a dependency to a separate useEffect hook that calls fetchData. In this case, the outcome is setting userId.
So instead of this:
useEffect(() => {
getData()
})
const getData = async () => {
await verifyUser()
await fetchData()
}
Do this:
useEffect(() => {
verifyUser();
}, []);
useEffect(() => {
if (userId) { // assuming userId has a false-y value before verifyUser resolved
await fetchData();
}
}, [userId])

Axios Delete doest not work in react-redux app

export const deleteComment = (id) => {
console.log("id", id);
return async (dispatch) => {
try {
const response = await axios.delete(
`http://localhost:5000/comments/${id}`
);
console.log("response", response);
dispatch(actions.DeleteCommentAction(id));
} catch (err) {
console.log(err);
}
};
};
For some reasons, this code does not work. When i use this acton the console.log("id",id) is being executed, but console.log with response is not. I tested my route for deleting in Postman and everything works, does anyone know what is this happening?
Route for deleting, but as i said. It work, it some issue in React i guess:
router.delete("/:id", async (req, res) => {
const id = req.params.id;
try {
Comment.findByIdAndDelete(id, function (error, response) {
if (error) {
return res.send(error);
}
console.log(response);
return res.send(response);
});
} catch (error) {
return res.send(error);
}
});
Have you inspected the network tab to see if the request was sent?
Can you share the component where you are invoking deleteComment?
And perhaps try to simplify and execute the deletion outside the deleteComment method, it might be you are not injecting the dispatch method into the anonymous function.
The cause could also related to redux. I guess you are not using a redux middleware to handle the asynchronous executions as you are waiting the deletion, which could be a possible scenario for the issue. And maybe you could consider adding one of those middlewares like reduxk-thunk or redux-saga for example.
I actually forgot to add:
const mapDispatchToProps = {
deleteComment,
};

An array of promises is returned when using axios in map function?

I am mapping through this dummyAssests array and want to create a new array of objects based on the axios response called newAssetArray. But when I console.log the newAssetArray it just contains an array of Promises...
useEffect(() => {
let newAssetArray = dummyAssets.map(async function (asset) {
return await axios
.get(currrentPrice(asset.asset_id))
.then((response) => {
let cp = response.data.market_data.current_price.eur;
let value = Number(cp) * Number(asset.amount);
return { ...asset, value: +value };
})
.catch((error) => console.log(error.response.data.error));
});
setAssets(newAssetArray);
console.log(newAssetArray);
}, [dummyAssets]);
Console:
Yes, that is expected. Use Promise.all() to wait for all the promises to resolve. .map() itself is not promise aware so it does not "wait" for each individual iteration of its loop to resolve before going onto the next one. So, you have the result of calling N async functions which all return a promise as your .map() result. That is how the code is supposed to work. The await you are using it not accomplishing anything. If you use a for loop instead (which is promise-aware, then your loop will be sequenced.
Here's how you could use Promise.all() to get the results:
useEffect(() => {
Promise.all(dummyAssets.map(function(asset) {
return axios
.get(currrentPrice(asset.asset_id))
.then((response) => {
let cp = response.data.market_data.current_price.eur;
let value = Number(cp) * Number(asset.amount);
return { ...asset, value: +value };
}).catch((error) => {
console.log(error.response.data.error)
throw error;
});
})).then(newAssetArray => {
console.log(newAssetArray);
setAssets(newAssetArray);
}).catch(err => {
// do something to handle the error here
});
}, [dummyAssets]);
Or, you might find this implementation a bit simpler to follow:
useEffect(async () => {
try {
const newAssetArray = await Promise.all(dummyAssets.map(async function(asset) {
const response = await axios.get(currrentPrice(asset.asset_id));
const cp = response.data.market_data.current_price.eur;
const value = Number(cp) * Number(asset.amount);
return { ...asset, value: +value };
}));
console.log(newAssetArray);
setAssets(newAssetArray);
} catch (e) {
// do something to handle the error here
console.log(e);
}
}, [dummyAssets]);
Also, note that a structure such as:
async function someFunction() {
return await axios.get()
}
does nothing different than just:
async someFunction() {
return axios.get()
}
They both return a promise that resolves to whatever axios.get() resolves to. The first returns a promise because it's in an async function and ALL async functions return a promise at the point they hit the first await. The second returns a promise because you're directly returning the axios.get() promise. Either way, they both return a promise that resolves to the same thing.
So, in general return await fn() is not helping you vs just return fn() and is not recommended. Then, once you stop using await there, you don't need async for that .map() callback anymore either.

Using useState hooks within an async callback

I'm running an async operation with a callback (ipfs in this case, but it probably doesn't matter), and I'm trying to set state using hooks within that callback. But the code isn't operating...do I need to use useEffect here maybe?
await ipfs.add(buffer, (err, ipfsHash) => {
setIpfsHash(ipfsHash);
console.log("in ipfs - ipfshash", ipfsHash);
});
setIpfsHash is blocking the inner code. The console.log does not run
The way you're handling your ipfs.add function with async/await is incorrect. But yes, you'd want to use React.useEffect in this situation.
If you want to use async/await, you need to create a new promise. In your example, ifps.add(buffer, callback) looks it's taking a callback as an argument, so this won't work.
Instead, turn it into a promise:
function add(buffer) {
return new Promise((resolve, reject) => {
const ipfs = ...
ipfs.add(buffer, (error, ipfsHash) => {
if (error) {
reject(error)
return
}
resolve(ipfsHash)
})
})
}
Then, you can use async/await in your React.useEffect like in the following example:
function App() {
const [buffer, setBuffer] = React.useState(null)
const [ipfsHash, setIpfsHash] = React.useState(null)
React.useEffect(() => {
async function doWork() {
try {
const result = await add(buffer)
setIpfsHash(result)
} catch (error) {
// handle error
}
}
doWork()
}, [buffer])
return (
<div>
...
</div>
)
}
Make sure to specify a dependency array for your useEffect hook so that it only runs when it needs to:
React.useEffect(() => {
// ...
}, [buffer])

Async/await redux thunk not returning promise to action correctly

I have a thunk using Axios that's posting to an Express route using Sequelize.
The route is posting correctly (ie. data is getting added to the db) but the action inside of the React component isn't behaving as expected. Using async/await, I expect the action to wait until it completes the db post before continuing but that's not the case here. I'm getting undefined from the action.
The thunk hits the express route where I'm dispatching the action to update my redux store and returning the response:
const addedNewList = (newList) => ({type: ADD_NEW_LIST, newList})
export const addNewList = (name, userId) => async dispatch => {
try {
const { data } = await axios.post('/api/list/add', { name, userId })
dispatch(addedNewList(data))
return data
} catch (err) {
console.error(err)
}
}
Using debugger, I can confirm that return data is in fact returning the response from the server that I need. I can also confirm that the redux store is getting updated correctly.
But here, when I try and access that response data as result, I get undefined:
handleSubmit = async () => {
const result = await this.props.addNewList(this.state.name, this.props.userId)
// ** result is 'undefined' **
this.handleClose()
// pass off the results
}
If I add a setTimeout after I evoke the addNewList action, it works as expected. This suggests to me that maybe it's not returning a promise? But my understanding was that if you returned the response from the server in the thunk, it would do that.
For completeness, here is my route which I've also confirmed with debugger that data is being passed as expected:
const userAuth = function(req, res, next) {
if (req.isAuthenticated()) {
return next()
}
res.status(401).send('Unauthorized user')
}
router.post('/add', userAuth, async (req, res, next) => {
const { name, userId } = req.body
try {
const list = await List.create({ name, userId })
res.json(list)
} catch(err) { next(err) }
})
Why is the action returning undefined in the handleSubmit method?
Try returning the dispatch of addedNewList(data) instead:
export const addNewList = (name, userId) => async dispatch => {
try {
const { data } = await axios.post('/api/list/add', { name, userId })
return Promise.resolve(dispatch(addedNewList(data)));
} catch (err) {
console.error(err)
}
}
That being said, you could consider restructuring the component to instead utilize mapStateToProps to use values/result from the updated Redux store rather than explicitly awaiting the response and manually passing the value?
The response from Alexander got me on the right track so I'm sharing my solution in case it helps someone (as he suggested).
While I could have continued to try and solve this by wrapping the dispatch in a Promise, the better solution was to rethink how the component was structured.
In my situation, I wanted to get the ID for the newly created row in the database so that I could pass it into history.push.
handleSubmit = async () => {
const result = await this.props.addNewList(this.state.name, this.props.userId)
this.handleClose()
history.push(`/list/${result.id}`)
}
With result coming back undefined, the url was not updating correctly.
The better solution was to access the new data from the redux store where it was updated. This way I could be certain the history wouldn't get updated until the data was ready.
So my updated component now looked something like this where the history wouldn't update until a newId was available:
handleSubmit = () => {
this.props.addNewList(this.state.name, this.props.userId)
this.handleClose()
}
render(){
const { newId } = this.props
if (newId) {
history.push(`/list/${newId}`)
}
return (
....
)
}
}
const mapStateToProps = (state) => {
return {
newId: state.list.newId
}
}
Instead of putting this into render, I could probably also use a component lifecylcle method like componentWillReceiveProps or similar.

Resources