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.
Related
I'm pretty new to ReactJS and redux, so I've never really had to work with this before. I'm retrieving data from an API call in my project. I want to modify the data by adding a new property to the object. However, because the code is not ran synchronously, the unmodified array is being returned (I assume) instead of the modified array.
export function loadThings() {
return dispatch => {
return dispatch({
type: 'LOAD_THINGS',
payload: {
request: {
url: API_GET_THINGS_ENDPOINT,
method: 'GET'
}
}
}).then(response => {
let things = response.payload.data;
// Retrieve all items for the loaded "things"
if(things) {
things.forEach((thing, thingIndex) => {
things[thingIndex].items = []
if (thing.hasOwnProperty('channels') && thing.channels) {
thing.channels.forEach(channel => {
if (channel.hasOwnProperty('linkedItems') && channel.linkedItems) {
channel.linkedItems.forEach(itemName => {
dispatch(loadItems(itemName)).then(
item => things[thingIndex].items.push(item) // push the items inside the "things"
)
})
}
})
}
})
}
things.forEach(data => console.log(data.items.length, data.items)) // data.items.length returns 0, data.items returns a populated array
return things // return the modified array
}).catch(error => {
//todo: handle error
return false
})
}
}
As you can see, I perform an API call which returns data named response. The array is populated with all "things". If things exists, I want to load extra information named "items". Based on the information in the things array, I will perform another API call (which is done by dispatching the loadItems function) which returns another promise. Based on the data in the results of that API call, I will push into the items property (which is an array) of the things object.
As you can see in the comments, if I loop through the things array and log the items property which I just created, it's basically returning 0 as length, which means the things array is being returned before the things array is being modified.
I would like to know two things:
What is causing my code to run async. Is it the
dispatch(loadItems(itemName)) function since it returns a promise?
How am I able to synchronously execute my code?
Please note: this function loadThings() also returns a promise (if you're not familair with redux).
You might be interested in knowing what I tried myself to fix the code
Since I fail to understand the logic why the code is ran async, I've been trying hopeless stuff. Such as wrapping the code in another Promise.all and return the modified array in that promise. I used the then method of that promise to modify the things array, which had the exact same result. Probably because return things is being executed outside of that promise.
I'd really love to know what is going on
Edit
I have added the loadItems() code to the question, as requested:
export function loadItems(itemName) {
return dispatch => {
const url = itemName ? API_GET_ITEMS_ENDPOINT + `/${itemName}` : API_GET_ITEMS_ENDPOINT;
return dispatch({
type: 'LOAD_ITEMS',
payload: {
request: {
url: url,
method: 'GET'
}
}
}).then(response => {
return response.payload.data
})
}
}
My approach would be to map over things, creating arrays of promises for all of their items wrapped in a Promise.all, which gives you an array of Promise.all's.
Then you return things and this array of promises in another Promise.all and in the next then block, you can just assign the arrays to each thing with a simple for loop:
export function loadThings() {
return dispatch => {
return dispatch({
type: 'LOAD_THINGS',
payload: {
request: {
url: API_GET_THINGS_ENDPOINT,
method: 'GET'
}
}
}).then(response => {
let things = response.payload.data;
// Retrieve all items for the loaded "things"
const items = things.map((thing) => {
const thingItems = []
if (thing.hasOwnProperty('channels') && thing.channels) {
thing.channels.forEach(channel => {
if (channel.hasOwnProperty('linkedItems') && channel.linkedItems) {
channel.linkedItems.forEach(itemName => {
thingItems.push(dispatch(loadItems(itemName)));
});
}
});
}
return Promise.all(thingItems);
});
return Promise.all([things, Promise.all(items)])
})
.then(([things, thingItems]) => {
things.forEach((thing, index) => {
thing.items = thingItems[index];
})
return things;
})
.catch(error => {
//todo: handle error
return false
})
}
}
Edit:
You need to push the dispatch(loadItmes(itemName)) calls directly into thingItems.
I guess you could refactor it like the following:
export function loadThings() {
return dispatch => {
return dispatch({
type: 'LOAD_THINGS',
payload: {
request: {
url: API_GET_THINGS_ENDPOINT,
method: 'GET'
}
}
}).then(response => {
let things = response.payload.data;
// Retrieve all items for the loaded "things"
if( things ) {
return Promise.all( things.reduce( (promises, thing) => {
if (thing.channels) {
thing.items = [];
promises.push( ...thing.channels.map( channel =>
channel.linkedItems &&
channel.linkedItems.map( item =>
loadItems(item).then( result => thing.items.push( result ) )
) ).flat().filter( i => !!i ) );
}
return promises;
}, []) );
}
return things;
}).catch(error => {
//todo: handle error
return false
})
}
}
In case you would have things, it would check for the channels and the linkedItems for that channel, and create a promise that will push the result back to the thing.items array.
By returning the Promise.all, the continuation of the loadThings would only complete when the Promise.all was resolved. In case there are no things, just things gets returned (which would be a falsy value, so I am wondering how valid that statement could be)
I haven't actually tested the refactoring so there might be some brackets in need of adjusting to your situation, but I guess it gives you an idea?
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...
Good day for all,
I am doing a React course and I'd submited the code to the reviewer. He's returned me few comments and there is one comment I'm not being able to solve.
The comment is the following:
Check if (query === this.state.query) to ensure you are not going to replace the contents to an old response
And part of the code is the one below:
updateQuery = (query) => {
this.setState({
query: query
})
this.updateWantedBooks(query);
}
updateWantedBooks = (query) => {
if (query) {
BooksAPI.search(query).then((wantedBooks) => {
if (wantedBooks.error) {
this.setState({ wantedBooks: [] });
} else {
this.setState({ wantedBooks: wantedBooks });
}
})
} else {
this.setState({ wantedBooks: [] });
}
}
Anyone could help me what do am I suppose to do?
Regards.
Code reviewer is right, you don't really want to replace the response if user has entered the very same query.
You have to store somewhere what for user has searched recently:
this.setState({ wantedBooks: [], query });
In case of success response:
this.setState({ wantedBooks, query });
And then check it in case of further searches:
if (query && query !== this.state.query) {
// continue the search only if query is different that current
Instead of relying on an outer member which is open to abuse by other code, you can employ a factory function to more safely trap a member.
As you have discovered, trapping and testing query == this.state.query can be made to work but is arguably not the best solution available.
With a little thought, you can force each call of updateWantedBooks() automatically to reject the previous promise returned by the same function (if it has not already settled), such that any success callbacks chained to the previous promise don't fire its error path is taken.
This can be achieved with a reusable canceller utility that accepts two callbacks and exploits Promise.race(), as follows:
// reusable cancellation factory utility
function canceller(work, successCallback) {
var cancel;
return async function(...args) {
if (cancel) {
cancel(new Error('cancelled')); // cancel previous
}
return Promise.race([
work(...args),
new Promise((_, reject) => { cancel = reject }) // rejectable promise
]).then(successCallback);
};
};
Here's a demo ...
// reusable cancellation factory utility
function canceller(work, successCallback) {
var cancel;
return async function(...args) {
if (cancel) {
cancel(new Error('cancelled')); // cancel previous
}
return Promise.race([
work(...args),
new Promise((_, reject) => { cancel = reject })
]).then(successCallback);
};
};
// delay utility representing an asynchronous process
function delay(ms, val) {
return new Promise(resolve => {
setTimeout(resolve, ms, val);
});
};
function MySpace() {
// establish a canceller method with two callbacks
this.updateWantedBooks = canceller(
// work callback
async (query) => delay(500, query || { 'error': true }), // a contrived piece of work standing in for BooksAPI.search()
// success callback
(wantedBooks => this.setState(wantedBooks)) // this will execute only if work() wins the race against cancellation
);
this.setState = function(val) {
console.log('setState', val);
return val;
};
};
var mySpace = new MySpace();
mySpace.updateWantedBooks({'value':'XXX'}).then(result1 => { console.log('result', result1) }).catch(error => { console.log(error.message) }); // 'cancelled'
mySpace.updateWantedBooks(null).then(result2 => { console.log('result', result2) }).catch(error => { console.log(error.message) }); // 'cancelled'
mySpace.updateWantedBooks({'value':'ZZZ'}).then(result3 => { console.log('result', result3) }).catch(error => { console.log(error.message) }); // {'value':'ZZZ'} (unless something unexpected happened)
Note that canceller() doesn't attempt to abort the asynchronous process it initiates, rather it stymies the success path of the returned promise in favour of the error path.
I think reviewer's point is that response of Search API is asynchronous and result for "query 1" can arrive after user changed his mind and already requested search "query 2". So when response arrive - we need to check if we really interested in it:
updateQuery = query => {
this.setState({
query: query
wantedBooks: []
})
this.updateWantedBooks(query);
}
updateWantedBooks = query => {
if (query) {
BooksAPI.search(query).then((wantedBooks) => {
// if updateQuery("query1) and updateQuery("query2") called in a row
// then response for query1 can arrive after we requested query2
// => for some period of time we'll show incorrect search results
// so adding check if query still the same can help
if (query !== this.state.query) {
// outdated response
return;
} else if (wantedBooks.error) {
// query is okay, but server error in response
this.setState({
wantedBooks: []
})
} else {
// success response to requested query
this.setState({ wantedBooks });
}
})
}
}
Guys I´ve done some tests with your answers, but I realize that somehow the code was behavioring strangely.
So, I've seen in other part of the reviewer comments, a part which I hadn't had seen before do my answer here, the following comment:
Inside 'then' part of the promise check if(query === this.state.query) to ensure you are not going to replace the contents to an old response.
And this "Inside 'then'" has been beating in my brain.
So, I think I've arrived in a satisfatory code; sure, maybe it isn't the definite solution, that's why I want to show here for you and feel free to comment if I'd have to make some improvement. Here below I put the code:
updateQuery = (query) => {
this.setState({
query: query
})
this.updateWantedBooks(query);
}
updateWantedBooks = (query) => {
if (query) {
BooksAPI.search(query).then((wantedBooks) => {
if (wantedBooks.error) {
this.setState({ wantedBooks: [] });
} else if (query !== this.state.query) {
this.setState( { wantedBooks: [] });
} else {
this.setState({ wantedBooks: wantedBooks });
}
})
} else {
this.setState({ wantedBooks: [] });
}
}
Regards
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;
},
I'm trying to setup a queuing system such that a post request is repeated n times, at intervals of i. Ideally I'd like to wrap this all in a single promise, so the whole thing can be asynchronous.
So far I have this, which feels somewhat messy and hacky. I feel like I may be missing a much easier solution, but I can't seem to find anything:
// this is mocked to avoid a wall of code
const postData = (url, data) => Promise.resolve(true);
// this is mocked to avoid a wall of code
const resIsFailed = () => true;
const requestChain = ({
url,
data,
maxRequests,
requestTimeout,
currentRequest = 0,
requestIncrement = increment => increment,
}) => {
// exit condition
if (currentRequest >= maxRequests || (!maxRequests)) {
console.log('Too many failed requests');
return Promise.reject(new Error('Too many attempts'));
}
// post the data, if it fails, try again
postData(
url,
data,
).then(res => {
if (resIsFailed(res)) {
console.log('Failed response: ');
console.dir(res);
setTimeout(() => {
requestChain({
url,
data,
maxRequests,
requestTimeout: requestIncrement(requestTimeout),
currentRequest: currentRequest + 1,
requestIncrement,
});
}, requestTimeout);
} else {
return Promise.resolve(res);
}
});
}
requestChain({
url: 'fail',
data: {},
maxRequests: 5,
requestTimeout: 100,
})
The async library is super helpful to control all kind of async chaining etc. You might want to have a look at async.retry. It should give you the exact behaviour.