I have a project which does not support generators and async await syntax.
I built the following code with async await because I don't see any other way to do it:
this.setState(async lastState => {
const newImagesAmount = lastState.images.length + 20;
const newImages = await this.getImages(this.props.tag, newImagesAmount);
return {
images: newImages
};
});
Why? In this particular case, the new state is built by both the old state and a result of a promise.
How can I transform it to non - async await syntax?
Note (Update):
Due to the fact that both the current answers contain the same bug, please read #dhilt answer + responses first which explain what are the bugs.
First of all it's impossible to use await inside of setState(). Not because of your project's limitation but because each function with await inside should be declared as async beforehand. And it should be invoked with await notation or with .then() chained. So callback passed into .setState should be async and called with await but we know that React does not work this way. React expect ordinary function and call it without await or .then(). It looks like func.apply(context, funcArgs) in react-dom module. Also there is no check if it returns Promise or not.
So let's return back to your final goal. As far as I understand it's about ensuring state is still consistent after deferred data is loaded. And here is another limitation. Except sync XHR(that is really bad practice) there is no way to pause all the execution until data comes. That is: whatever you do you cannot be 100% sure state has not mutate between "request has been sent" and "data is received" steps. Neither await, nor functional setState does not allow freeze execution. Yes, your code is still referencing to "prevState" variable but it could be not actual component's state anymore!
So what you could do:
abort ongoing XHR before sending another one to the same endpoint(should work for read operations; does not make sense for write operations)
parse request params when processing response to inject it into state in valid way(say, first response is for 1..10 images and next one is for 11..20 - so you can work with that and inject second response into appropriate indexes leaving first 10 still unintialized)
set up some flag to block new requests until ongoing is finished
P.S. so making request from inside setState function leads to confusion and don't add any additional flexibility.
If the code in the question is correct and you just want to purge async/await in favour of traditional return promise.then(...) syntax, then do as follows:
this.setState(lastState => {
const newImagesAmount = lastState.images.length + 20;
return this.getImages(this.props.tag, newImagesAmount)
.then(newImages => ({ 'images': newImages });
});
The following simplification may also work :
this.setState(lastState => {
return this.getImages(this.props.tag, lastState.images.length + 20)
.then(images => ({ images });
});
Im assuming that lastState here is really just the current state before you update it. Try something like this:
const imageLength = this.state.images.length + 20
this.getImages(this.props.tag, imageLength)
.then(newImages => {
this.setState({images: newImages})
}
I think you may want something like this
const newImagesAmount = this.state.images.length + 20;
this.getImages(this.props.tag, newImagesAmount).then(newImages =>
this.setState(prevState => ({
..prevState,
images: newImages
})
);
Firstly, you do a request for new images and this request is based on current state. Then (when the result is arrived) you update state.
Updated version that allows to protect async logic from multiple executions before the request is finished
if (!this.state.pending) {
this.setState(lastState => {
const newImagesAmount = lastState.images.length + 20;
this.getImages(this.props.tag, newImagesAmount).then(newImages =>
this.setState(prevState => ({
...prevState,
pending: false,
images: newImages
})
).catch(() =>
this.setState(prevState => ({
...prevState,
pending: false
})
);
return {
...lastState,
pending: true
}
});
}
Related
I am new to JavaScript and have been trying to read up a lot on why this is not working. Here is my code. I have also read a number of articles here on stack overflow but still feeling dense
Also if my title does not make sense, please suggest an edit
listRef.listAll()
.then(response => {
let files = []
response.items.forEach(item => {
var text
getText(item.name).then(res=>{text = res});
const id = {uid: guid()}
const url = item.getDownloadURL().then(url => {return url} )
const gsurl = `gs://archivewebsite.appspot.com/${folder}/${item.name}`
files.push({...item, name:item.name, url, gsurl, id:id.uid, text})
});
this.files = files;
})
.catch(error => console.log(error));
async function getText(docID) {
var docRef = firestore.collection("recipes").doc(docID);
let doc = await docRef.get()
if (doc.exists){
return doc.data().text
}
}
that code "works" in that it logs the response to the console but the text variable is a pending promise object.
I understand that async functions return a promise so when I call getText I need to use .then - what I am struggling with and have refactored this code a few times is this:
how can I assign the value of doc.data().text to a variable to be used later in other words, how can var text be an actual string and not a promise object pending
Also for my own learning on javascript inside the async function if I replace
if (doc.exists){
return doc.data().text
}
with
if (doc.exists){
return Promise.resolve(doc.data().text)
}
I get the same result in console.log - is this expected? is return simply short hand for the handler to resolve the promise?
I have also refactored this code to be non async and I get the same result where my var text is basically a pending promise and never the resolved data
Thanks for your help - also any articles to help explain this to me would be great! I have been going through courses on udemy but little confused by this right now
Actually you are assigning the complete promise to the variable text
Replace
var text = getText(item.name).then(res=>console.log(res))
by
var text = await getText(item.name);
OR
var text
getText(item.name).then(res=>{text = res});
Of course text is going to be a Promise. Promise.then() always returns a Promise.
Consider this code:
function doA(n) {
// do something here...
console.log("A" + n);
}
asnyc function doB(n) {
// do something here...
console.log("B" + n);
}
doA(1);
doA(2);
doB(3); // async
doA(4);
doB(5); // async
doB(6); // async
doA(7);
What do you expect the output to be?
1, 2, 4, and 7 will always be in order, because they are executed synchronously.
3 will not ever print before 1 and 2. Likewise, 5 and 6 will not ever print before 1, 2, and 4.
However, 3, 5, and 6 can be printed in any order, because async functions do not guarantee execution order once created.
Also, 4 can print before 3. Likewise, 7 can print before 5 and 6.
Basically, think of async functions as a parallel task that runs independently (although not really; single thread JS only simulates this behavior). It can return (fulfill/reject) at any moment. For this reason, you cannot just simply assign a return value of an async function to a variable using synchronous code - the value is not guaranteed to be (and probably is not) available at the moment of synchronous execution.
Therefore you need to put all the code that requires the value of text to be set into the callback block of the Promise, something like this:
getText(item.name).then((text) => {
// put everything that uses text here
});
This can of course lead to the infamous "callback hell", where you have layers inside layers of async callback. See http://callbackhell.com for details and mitigation techniques.
async/await is just one of the newer ways to do the same thing: MDN has an excellent article here: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await
OK I worked with someone at work and found a solution - it was related to this post
https://stackoverflow.com/a/37576787/5991792
I was using async function inside a for each loop
the refactored code is here
async function buildFiles(){
let items = await listRef.listAll()
let files = []
for (const item of item.items){
const text = await getText(item.name)
const url = await item.getDownloadURL()
const gsurl = `gs://archivewebsite.appspot.com/${folder}/${sermon.name}`
files.push({...item, name:item.name, url, gsurl, text})
}
return files
}
async function getText(docID) {
var docRef = firestore.collection("recipies").doc(docID);
let doc = await docRef.get()
if (doc.exists){return await doc.data().text}}
buildFiles().then(res=>this.files = res)
Thanks also to #cyqsimon and #Nav Kumar V
At a loss here. I've tried many variations of this but basically I am trying to set the state of uploadRefId to the id returned once I create a new collection. I can see the id is generated via console.log but the setState function does not seem to work within the scope of this promise (the uploadRefId is null)?
const [uploadRefId, setUploadRefId] = React.useState(null);
let uploadRefCol = await firestore.collection("uploads");
await uploadRefCol.add(uploadData)
.then( (uploadDataRef) => {
console.log('uploadDataRef.id: ', uploadDataRef.id);
setUploadRefId(uploadDataRef.id);
console.log('uploadRefId', uploadRefId);
})
.catch( (error) => {
console.log('Error!', error)
});
Changing state can be funny throughout the life cycle of React.
If your uploadDataRef.id is of type string see if this helps:
const [uploadRefId, setUploadRefId] = React.useState('');
If it is a numeric value see if setUploadRefId works with this:
const [uploadRefId, setUploadRefId] = React.useState(0);
This may not be a solution per say but it could lead you to one.
setUploadRefId(uploadDataRef.id);
Above is async update state so if you immediately use state value then the value is yet not set and you are trying to access the value
useEffect(() => {
console.log('uploadRefId', uploadRefId);
})
So above use effect runs on every update in the state you can watch out for any state change
Code sandbox sample
I am developing a nodejs application that needs to get settings from an array(in a settings object), call a rest api based on the settings and write the response to mongodb and repeat this for the next setting in the array.
Here is a simplified version of the application
var setting //global
process(){ //top level function
for(let s of config.settings){
setting = s;
getData();
}
}
function getData(){
init()
.then(makeRequest) // constructs and makes the rest api call
.then(insert) // writes the response to the db
.catch(function(err){
// logs err
}
}
Running it, only the data for the last setting (in the array) is written to the db and this happens for each iteration. Basically the same data is written on the db for as many iterations.
The problem I can see from this is that the for loop finishes executing, before the promises return with the value.
I have seen some examples of async.for
Any suggestions on fixing this. How do you go about designing this kind of a flow?
You can bind the settings to each function call to preserve the value. looks like you'd have to refactor though as the value would be passed in as an argument though i'm not sure if your code is pseudo code or actual code.
async await would work as well but would take longer as it would pause execution at each api call.
You should return an object or array that you can use to store an internal state for your request. Please see the example for how it works.
Also never set a global variable to store your state, with your function being asynchronous the value may not be what you expect it to be.
With this approach you are passing { init } for the first promise, then { init, request } for the next so you have the response from each part of your promise chain that you can use to make further requests.
// return an object to store the state on init
const init = () =>
new Promise((res, rej) => res({
init: 'initted'
}))
// pass init and the request to the next function in the chain
const makeRequest = ({ init }) =>
new Promise((res, rej) => res({
init,
request: {
msg: 'this is the response',
id: 33
}
}))
// insert stuff from the request
// then return the data to the next query
const insert = ({ init, request }) =>
new Promise((res, rej) => res({
request,
init,
created_at: Date.now()
}))
const trace = name => x => (console.log(name, x), x)
function getData(){
return init() // return your promise so you can chain it further
.then(trace('after init'))
.then(makeRequest)
.then(trace('after request'))
.then(insert)
.then(trace('after insert'))
.catch(console.error)
}
// call you function
getData()
// since the promise is returned we can continue the chain
.then(state => console.log({ state }))
<script src="https://codepen.io/synthet1c/pen/KyQQmL.js"></script>
All of your loop will have executed by the time the callbacks are coming in. So settings will be the last value.
Instead of relying on globals, pass setting into getData, for example.
In one of my React components, I have the following method:
save() {
var arrLength = this.state.arr.length;
if(arrLength > 1) {
var start = this.state.arr[arrLength-2];
var end = this.state.arr[arrLength-1];
this.props.updateItemAudio(start,end);
this.props.saveItemToDisk();
this.props.getNextItem();
}
else {
console.log("Array not long enough yet");
}
}
The important bit is this:
this.props.updateItemAudio(start,end);
this.props.saveItemToDisk();
this.props.getNextItem();
I need to wait for the first function call it finish, before executing the second, and it's the same thing with the third. How can I achieve this? I heard that there is this new async / await syntax, could i use that here somehow? I would prefer not to pass callback functions to each of these actions.
Edit: My methods do not return promises, they are normal Redux-actions.
If yours methods return promise you can use:
async save() {
await this.props.firstMethod();
await this.props.secondMethod();
await this.props.thisMethod();
}
Documentation: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
Your bundler or other task runner should transpile EcmaScript7 remember that
You could execute them one after another inside of the first action, i.e.:
updateItemAudio = async (start, end) => {
return (dispatch) => {
try {
// updateItemAudio stuff...
let resultTwo = await saveItemToDisk();
let resultThree = await getNextItem();
// now all 3 executed in order
} catch (err) {
console.log(err);
}
}
Pure redux does not allows you to dispatch an action after another action in a clean way.
I'll suggest to use 3rd party libraries for that, such as redux-observable or redux-saga. They fits perfect for your use case.
You can solve this problem using redux-observable in this way:
const myEpic = combineEpics(
// listening to UPDATE_ITEM_AUDIO action type and dispatching saveItemToDisk() action after it
action$ => action$.ofType(UPDATE_ITEM_AUDIO).map((action) => saveItemToDisk()),
// the same as above, but listening for SAVE_ITEM_TO_DISK, and dispatching getNextItem() action
action$ => action$.ofType(SAVE_ITEM_TO_DISK).map((action) => getNextItem())
)
I understand this is a very common problem in RN and I am still trying to understand the very possible advantage of returning a promise when loading data from a property file instead of just returning the value, which makes chaining requests very cumbersome...but anyway. Here is what I have right now, which is a wrapper from the AsyncStorage RN implementation:
multiGet = async (key) => {
var value = null;
try {
value = await AsyncStorage.multiGet(key).then(
(values) => {
value = values;
console.log('Then: ',values);
});
} catch (error) {
console.log('Error: ',error);
}
console.log('Final: ',value);
return value;
}
At this point, value gets undefined. In my main code I have this:
var filter = this._getFilter();
console.log('returned filter:',filter);
The _getFilter function is the one using the AsyncStorage wrapper but the 'returned filter' is logging before the first function so it is not waiting for the returned values before continue, so I get an undefined value.
At first, I thought that just by using the async/await the AsyncStorage wold return a value instead of a promise but after testing, the value I get from:
value = await AsyncStorage.getItem('key')
is STILL a promise, so I have to use then() to read the value.
Basically the order that I am seeing in the logs is:
_getFilter
returned value: undefined
Then: value: here I get the correct value from the keys but the code already passed and I don't have the correct value in the variable
I have no clue what is going on or how to handle this correctly. This is supposed to be very simple and common use case.
I would love to solve this without using a third party module.
Thanks
SOLUTION
Edit: After understanding a little more about the concepts of async/await and callbacks, I finally have a code that works. I don't like it, because it makes the code very hard to read. I might need to refactor it to use promises but for now, it works. Here are some snippets in case someone finds the same issue:
this._getFilter(body,this._filterSearchCallback,callback);
Note: I am sending the body through the chain because I am "completing" the information as I pass the functions. The second parameter is the first callback that actually makes a fetch query and the third callback is the return of the fetch function.
_getFilter(body,callback,returnCallback){
{...}
this._sh.multiGet(keysBanks).then(
(banks) => {
filter.banks = banks;
console.log(banks);
this._sh.multiGet(keysCards).then(
(cards) => {
console.log(cards);
filter.credit_cards = cards;
callback(body,filter,returnCallback);
});
}
);
}
Here basically I am chaining a couple of gets because I need several values from the store. This is the part I dont really like. _sh is my StorageHelper which is a wrapper to the AsyncStorage, nothing fancy.
multiGet = async (key) => {
const value = await AsyncStorage.multiGet(key);
return value;
}
Then my very last callback that actually makes the fetch and send the JSON response to the main screen in react native:
_filterSearchCallback(body,filter,callback){
body.filter = filter;
return fetch(apiUrl, {method: 'post', body: JSON.stringify(body)})
.then((response) => response.json())
.then((responseJson) => {
callback(responseJson);
})
.catch((error) => {
console.error(error);
callback(responseJson);
});
}
I will improve this and make it cleaner but for now, it works. Hope it helps others too.
Once upon a time, i was having the same problem so what I did I will share with you here.
Basically, your execution is moving forward without taking any value i.e undefined what you are getting right now so there are 3-4 ways to get out of this:
1) async await
2) callback
1) We will start with the callback which is use by most of the people.
We will use your code to implement this:
_getFilter(key,callback)
{
multiGet = (key) => {
var collect;
try {
var value = AsyncStorage.multiGet(key).then(
(values) => {
// value = values;
console.log('Then: ',values);
callback(values)
});
} catch (error) {
console.log('Error: ',error);
}
console.log('Final: ',value);
}
}
this._getFilter(key,function(filter){
console.log('returned filter:',filter);
});
2)async/await
If you are using await alone then you would get an error, to use await inside a function you have to declare the async function by setting async keyword before the function name.
async _getFilter(key)
{
multiGet = async (key) => {
var value,collect;
try {
value = await AsyncStorage.multiGet(key).then(
(values) => {
collect= values;
console.log('Then: ',values);
});
} catch (error) {
console.log('Error: ',error);
}
console.log('Final: ',value);
return collect;
}
//calling the async function
this._getFilter(key).then((filter)=>{
if(filter!=null)
console.log('returned filter:',filter)
else
console.log('error')
})
Hope this would clear your concepts and help you with other react native developers.I have seen lots of people struggling with this thing so today I got the chance to clear your doubts.
Cheers :)
the thing is await turns the promise into a value, you don't need to use .then(). Try the following:
const keys = await AsyncStorage.getAllKeys()
const values = await AsyncStorage.multiGet(keys)
// at this point `values` will have data for all keys