Async/await with sync func changing react state - javascript

I get problems with async/await functions and changing state in React.
This is my async function, which is triggered by clicking on the button:
async startNewEntry() {
this.addIssue();
let issue_id;
console.log(this.state.timeEntry, "started_timeEntry")
if (this.state.timeEntry?.issue?.id) {
issue_id = this.state.timeEntry?.issue?.id;
} else {
issue_id = (await this.issueService.list()).data[0]?.id;
}
const { data } = await this.timeEntryService.create({
comments: this.state.timeEntry.comments,
issue_id,
spent_on: moment(new Date()).format("YYYY-MM-DD"),
hours: 0.01,
activity_id: this.localStorageService.get("defaultActivityId")
});
In this function I use this.addIssue, which use this.createIssue, which changing my class component state:
addIssue() {
this.projectService.list([]).then(response => {
response.data = response.data.filter((x: any) => x.status === 1);
this.setState(
{
activeProjects: response.data
},
() => {
this.createIssue();
}
);
});
}
createIssue() {
this.issueAddService
.create({
project_id: this.state.activeProjects[0].id,
tracker_id: trakerId,
priority_id: priorityId,
subject: this.state.timeEntry.comments,
description: this.state.timeEntry.comments
})
.then(response => {
let timeEntry = this.state.timeEntry;
timeEntry.issue = response.data;
this.setState({
timeEntry
});
})
.catch(error => {
console.log("error", error);
});
}
As you can see, in my async function I new to have my new State, but actually async function works before my this.addIssue function. I know that question might be little freaky, but Thanks in forward!!

I am not a React expert, but I don't fully understand why there are lot of setState invocations spread around the place.
If you leave the setState to the end of the function, then you might not need to worry about correctly sequencing asynchronous calls to it (although the other answer does show how this can be achieved).
Perhaps invoking it once might make the code clearer. I welcome corrections...
async startNewEntry() {
const activeProjects = await fetchActiveProjects()
const issue = await this.createIssue()
const timeEntry = await createTimeEntry({ issue, comments: this.state.timeEntry.comments })
this.setState({ activeProjects, issue, timeEntry })
}
async fetchActiveProjects() {
const { data } = await this.projectService.list([])
return data.filter(({ status }) => status === 1)
}
async createIssue() {
const { data } = await this.issueAddService.create({
project_id: this.state.activeProjects[0].id,
tracker_id: trakerId,
priority_id: priorityId,
subject: this.state.timeEntry.comments,
description: this.state.timeEntry.comments
})
return data
}
async createTimeEntry({issue, comments}) {
const { data } = await this.timeEntryService.create({
comments,
issue_id: issue?.id || (await this.issueService.list()).data[0]?.id,
spent_on: moment(new Date()).format("YYYY-MM-DD"),
hours: 0.01,
activity_id: this.localStorageService.get("defaultActivityId")
})
return data
}
You can probably speed this up further by parallelizing the first two async calls:
async startNewEntry() {
const [activeProjects, issue] =
await Promise.all([fetchActiveProjects(), this.createIssue()])
const timeEntry = await createTimeEntry({ issue, comments: this.state.timeEntry.comments })
this.setState({ activeProjects, issue, timeEntry })
}

If you want startNewEntry to wait to do its work until after addIssue has done its work, you need to:
Have addIssue return a promise it fulfills when it's finished its work, and
Use await when calling it: await this.addIssue();
If you need startNewEntry to see the updated state, addIssue's promise will need to be fulfilled from the state completion handler callback, like this:
addIssue() {
// *** Return the promise chain to the caller
return this.projectService.list([]).then(response => {
response.data = response.data.filter((x: any) => x.status === 1);
// *** Create a new promise
return new Promise(resolve => {
this.setState(
{
activeProjects: response.data
},
() => {
this.createIssue();
resolve(); // *** Fulfill the promise
}
);
});
});
}
Often, new Promise is an anti-pattern, particularly when you have another promise you can chain from. But in this case, since you need to wait for the callback from setState (which isn't promise-enabled), it's appropriate. (
Note my comment on the question. I think you're setting up an endless loop...

Related

Declare an Async function

I'm new in React and I'm trying to call an async function, but I get the Unexpected reserved word 'await'. ERROR
So my async function is in a Helper class who is supposed to do all the API call, and I'm calling this Helper class in my Game function
Here is my code from Class Helper :
class Helper {
constructor() {
this.state = {
movieAnswer: {},
profile_path:"",
poster_path:"",
myInit: { method: "GET", mode: "cors" }
}
}
fetchMovieFunction = async (randomMovie) => {
return new Promise((resolve) => {
fetch(`${MOVIE_KEY}${randomMovie}?api_key=${API_KEY}`, this.myInit)
.then(error => resolve({ error }))
.then(response => resolve({ poster_path: response.poster_path }));
});
}
and here is where I call fetchMovieFunction in my game function :
const helper = new Helper();
function Game() {
useEffect(() => {
setRandomMovie(Math.floor(Math.random() * (500 - 1) + 1));
const { poster_path, error } = await helper.fetchMovieFunction(randomMovie)
if (error)
return console.log({error});
setApiMovieResponse({poster_path})
}, [page]);
return ();
}
And I don't understand why on this line const { poster_path, error } = await helper.fetchMovieFunction(randomMovie) I get the Unexpected reserved word 'await'. Error like my function is not declare as an async function
two issues:
You should use await inside an async function
But the callback passed to useEffect should not be async. So, you can create an IIFE and make it async:
useEffect(() => {
(async () => {
setRandomMovie(Math.floor(Math.random() * (500 - 1) + 1));
const { poster_path, error } = await helper.fetchMovieFunction(randomMovie);
if (error) return console.log({ error });
setApiMovieResponse({ poster_path });
})();
}, [page]);
The callback itself shouldn't be async because that would make the callback return a promise and React expects a non-async function that is typically used to do some cleanup.
You will necessarily need to wrap the hook callback logic into an async function and invoke it. React hook callbacks cannot be asynchronous, but they can call asynchronous functions.
Also, since React state updates are asynchronously processed, you can't set the randomMovie value and expect to use it on the next line in the same callback scope. Split this out into a separate useEffect to set the randomMovie state when the page dependency updates, and use the randomMovie as the dependency for the main effect.
useEffect(() => {
setRandomMovie(Math.floor(Math.random() * (500 - 1) + 1));
}, [page]);
useEffect(() => {
const getMovies = async () => { // <-- declare async
const { poster_path, error } = await helper.fetchMovieFunction(randomMovie);
if (error) {
console.log({error});
} else {
setApiMovieResponse({poster_path});
}
};
if (randomMovie) {
getMovies(); // <-- invoke
}
}, [randomMovie]);
FYI
fetch already returns a Promise, so wrapping it in a Promise is superfluous. Since you are returning the Promise chain there is also nothing to await. You can return the fetch (and Promise chain).
fetchMovieFunction = (randomMovie) => {
return fetch(`${MOVIE_KEY}${randomMovie}?api_key=${API_KEY}`, this.myInit)
.then(response => return {
poster_path: response.poster_path
})
.catch(error => return { error });
}
In order to do so, It's not suggested to asynchronize a function as a callback function being used in useEffect() since Effect callbacks are synchronous to prevent race conditions.
the better approach would be inside the useEffect callback
check this out:
useEffect(() => {
async function [name] () {
.....
.....
}
[name]();
},string[])
fetchMovieFunction doesn't need to be async because you're already returning a promise. Instead just return the fetch (also a promise), and catch any errors from that call in that method:
fetchMovieFunction = (randomMovie) => {
try {
return fetch(`${MOVIE_KEY}${randomMovie}?api_key=${API_KEY}`, this.myInit)
} catch(err) {
console.log(err);
}
});
But your useEffect, or rather the function you should be calling within the useEffect, does need to be async (because you're using await, but now you can just pick up the data from the fetch when it resolves.
useEffect(() => {
async function getData() {
setRandomMovie(Math.floor(Math.random() * (500 - 1) + 1));
const { poster_path } = await helper.fetchMovieFunction(randomMovie);
setApiMovieResponse({ poster_path });
}
getData();
}, [page]);

Promise Map keeps returning null

Been on this for over 24 hours, everything seems to work as I want but the promise keeps returning null.
[
null,
null
]
here are my codes:
let vettedBatch = currentBatch.Items.map((current) => {
getUser(current.userId).then((res) => {
// return resolve(JSON.parse(res.body));
let body = JSON.parse(res.body);
if (body.hasOwnProperty("Item")) {
return body;
} else {
//if user does not exist on the users table based on ID, lets take his transaction out of the fail-safe table
console.log(
`user with id number ${current.userId} with transaction id ${current.txId} do not exist or must have been disabled`
);
User.deleteWithdrawalTx(current.txId).then(() => {
console.log(
`transaction id ${current.txId} delete for unknown user with userId ${current.userId}`
);
});
}
});
});
You need to use Promise.all:
const data = [];
const promises = currentBatch.Items.map(async current => {
return await getUser(current.userId)
});
Promise.all(promises)
.then(res => {
res.map(item => {
let { body } = JSON.parse(item);
if (body.hasOwnProperty("Item")) {
data.push(body);
} else {
console.log('message');
}
})
})
Instead of mixing async/await and Promise syntax, I would suggest you to stick to one.
Here would be your code written fully in async/await syntax:
const getData = async () => { // async function instead of new Promise()...
return Promise.all(currentBatch.Items.map(async (current) => { // make map async and await it with Promise.all()
const res = await getUser(current.userId); // await instead of .then()
let body = JSON.parse(res.body);
if (body.hasOwnProperty("Item")) {
return body;
} else {
console.log(`user with id number ${current.userId} with transaction id ${current.txId} do not exist or must have been disabled`);
await User.deleteWithdrawalTx(current.txId); // await instead of .then()
console.log(`transaction id ${current.txId} delete for unknown user with userId ${current.userId}`);
// You should return something here too, but I dont know what you want to return, so...
}
}));
}
let vettedBatch = await getData(); // await the async function
Your problem is actually a deviation of this question: How do I return the response from an asynchronous call?. Should be fixed in my answer, but I still suggest you to check out the linked thread.

How to keep code synchronous when there are asynchronous calls inside of loops

I'm filling an array inside a loop and I need the full array when the loop finishes.
I've tried handling everything through promises or by using a counter but i can't seem to figure out the trick here.
lambda.listFunctions({}).promise()
.then((data) => {
data.Functions.forEach(func => {
lambda.listTags({ Resource: func.FunctionArn }).promise()
.then((data) => {
if ("Edge" in data.Tags) {
available_functions.push(func.FunctionName)
}
})
});
console.log(available_functions)
})
available_functions is always empty unless I console log it at the end of each foreach loop and then I have it returning 18 times which is not what I want.
I believe you can just promise it chain it to ensure all operations within the scope of the then completes before going down the chain.
lambda.listFunctions({}).promise()
.then(data => {
const { Functions } = data;
// I converted this from forEach to for of
for (const func of Functions) {
lambda.listTags({ Resource: func.FunctionArn }).promise()
.then(data => {
if ("Edge" in data.Tags) {
available_functions.push(func.FunctionName)
}
})
}
// also you can promise chain it if available_functions is within scope
})
.then(() => console.log(available_functions))
Or the cleaner async await way would look something like...
async fn() {
const available_functions = [];
const { Functions } = await lambda.listFunctions({}).promise();
for (const func of Functions) {
const tags = await lambda.listTags({ Resource: func.FunctionArn }).promise();
if ("Edge" in tags) {
available_functions.push(func.FunctionName)
}
}
return available_functions
}
Hope this helps!
You can use Promise.all with your problem. See documentation on Promise.all().
const available_functions = [];
lambda.listFunctions({}).promise()
.then((data) => {
const promises = []; // Collect promises
data.Functions.forEach(func => {
promises.push(lambda.listTags({ Resource: func.FunctionArn }).promise()
.then((data) => {
available_functions.push(func.FunctionName)
return Promise.resolve(available_functions);
})
);
});
Promise.all(promises)
.then(results => {
console.log(available_functions)
// or
console.log(results[results.length - 1]);
});
});

Async method not waiting for a function - VUE

i'm having this error and haven't got to resolve it though have researched a lot in MDN and here. As title saysinto VUE i'm trying to use async and await but js is not waiting the 'await' function to end. Here it is:
methods: {
async search (terms, done) {
console.log('1.')
this.filter = this.$refs.chipsInput.input
await this.loadtags()
console.log('3.')
done(this.tagsList)
},
loadtags () {
this.$axios
.get('/api/tags/?id__icontains=&id=&name__icontains=' + this.filter + '&name=&ordering=name&page_size=20')
.then(response => {
console.log('2.', response.data.results)
let temp = response.data.results
this.tagsList = temp.map(obj => {
return {
name: obj.name,
label: obj.name,
value: obj.name,
idField: obj.id
}
})
})
},
I am not able to post pictures yet, but add a link where you can look the console log where js prints the '3.' (which is placed after the await call) before '2.':
Image:
console
¿What am i doing wrong? already tried modifying the await like this:
let foo = await this.loadtags() and including a 'return 0' at the end of loadtags function but didn't work for me. Probably is a dumb thing, excuse me for that.
You aren't returning anything from the loadtags method, so the code doesn't wait.
Change this:
loadtags () {
this.$axios
.get(...
To this:
loadtags () {
return this.$axios
.get(...
async/await is more or less just sugar over Promises, so returning the Promise gives you something to await in the other method.
This is how I resolved this in my Vue application.
Before a user submits a new "tag" with submitNewTag() I need to check if it exists already in the list of tags, using async theTagExists().
submitNewTag() {
this.clearError();
this.theTagExists().then((res) => {
if (!res) {
console.log("TAG DOES NOT EXIST, SO ADD IT TO THE DATABASE");
this.saveTagToDatabase();
}
});
},
async theTagExists() {
console.log("CHECKING IF A TAG EXISTS");
await axios.get(`${this.apiUrl}/alltags`).then((res) => {
console.log("CHECKING IS DONE");
this.tagExists = res.data.allTags.some(
res =>
res.name.trim().toLowerCase() ===
this.newTag.tagName.trim().toLowerCase()
);
});
console.log("RETURN THE RESULT");
return this.tagExists;
},

Reactjs - ordering if else and outer block

I have a react component which has the following functions:
alreadyUpvoted() {
return this.state.upvotes.indexOf(this.props.context.userId) !== -1
}
alreadyDownvoted() {
return this.state.downvotes.indexOf(this.props.context.userId) !== -1
}
addUpvote() {
this.setState(prev => ({
upvotes: prev.upvotes.concat(this.props.context.userId),
upvoted: true,
votes: prev.votes + 1,
}), () => {
console.log('add upvote', this.state)
})
}
removeUpvote() {
var new_upvotes = this.state.upvotes.concat()
new_upvotes.pop(this.props.context.userId)
this.setState(prev => ({
upvotes: new_upvotes,
upvoted: false,
votes: prev.votes - 1,
}), () => {
console.log('remove upvote', this.state)
})
}
addDownvote() {
this.setState(prev => ({
downvotes: prev.downvotes.concat(this.props.context.userId),
downvoted: true,
votes: prev.votes - 1,
}), () => {
console.log('add dowvote', this.state)
})
}
removeDownvote() {
var new_downvotes = this.state.downvotes.concat()
new_downvotes.pop(this.props.context.userId)
this.setState(prev => ({
downvotes: new_downvotes,
downvoted: false,
votes: prev.votes + 1,
}), () => {
console.log('remove downvote', this.state)
})
}
postVotesData() {
var json = {
upvotes: this.state.upvotes,
downvotes: this.state.downvotes
}
json = JSON.stringify(json)
console.log(json)
const url = `/api/reddit/r/${this.props.subreddit}/posts/${this.props.postid}/`
fetch(url, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: json
})
.then(response => {
console.log('response status:', response)
return response.json()
})
.then(res => console.log('response data:', res))
}
I have created a toggleUpvote function which utilizes all the above functions:
toggleUpvote() {
if (this.alreadyDownvoted()) {
this.removeDownvote()
this.addUpvote()
}
else if (this.alreadyUpvoted()) {
this.removeUpvote()
}
else {
this.addUpvote()
}
this.postVotesData()
}
The problem here is the this.postVotesData() is getting executed before the if-else block finished.
For the current state, the control should go to else block. But, The console.log in this.postVotesData() got executed before the console.log present in addUpvote!!
evidence:
At the current state, upvotes array should have one value after executing of else and that array should be used in PUT. But, empty array is being PUT and then value gets added to array.
I also want the functions inside if block to be executed in order. How can I solve this?
That is how asynchronous javascript works. I believe you are doing an asynchronous operation like a fetch call, or react's setState(). Both of them(and many other things) in js are async.
To deal with this, you need to use callbacks or promises. When using promises, you can use the async-await syntax to have a clean code.
async toggleUpvote() {
if (this.alreadyDownvoted()) {
await this.removeDownvote()
await this.addUpvote()
}
else if (this.alreadyUpvoted()) {
await this.removeUpvote()
}
else {
await this.addUpvote()
}
await this.postVotesData()
}
To do this, removeUpvote, addUpvote and postVotesData need to return promises.
If you calling the setState in these functions, you need to provide a callback to setState. Or else you can use the functional-setState pattern.
A simple fix to avoid this problem is to return a promise from your functions similar to the following, and use the async-await syntax as suggested above.
addUpvote() {
return new Promise((resolve, reject) => {
this.setState(prev => ({
upvotes: prev.upvotes.concat(this.props.context.userId),
upvoted: true,
votes: prev.votes + 1,
}), () => {
console.log('add upvote', this.state)
return resolve(); // Signal that the operation has finished
})
})
}
It may appear like that, but it does not get excecuted before the block is done. All these functions are started in that exact order (synchrone), depending on which if/else route.
The reason you may think that, is because these functions are probably async, like an AJAX request. Those work with promises and/or on-complete functions. You might want to look up some tutorials about that, as it's to broad to explain in a simple answer here.
Simply put: With a promise you tell javascript to wait for the result before continueing.
edit: I noticed the react tag, you might want to check out await. It might take a few reads to understand what is going one, but worth the research.

Categories

Resources