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
Related
I have two calls to Firebase: one to get the existing data and one to listen for updates in the data. When those updates happen, instead of replacing the existing data for some reason I see to be adding the two datasets together. Can't figure out why as I'm directly updating state with new data in my second function.
Here are the functions called on mounted():
mounted() {
this.getImages();
this.refreshImages();
},
And the two functions in question:
async getImages() {
let snapshot = await db
.collection("Maria")
.orderBy("timestamp", "desc")
.get();
snapshot.forEach((doc) => {
let appData = doc.data();
appData.id = doc.id;
this.picturesData.push(appData);
});
this.dataLoaded = true;
},
async refreshImages() {
await db
.collection("Maria")
.orderBy("timestamp", "desc")
.onSnapshot((snapshot) => {
let newPicturesData = [];
snapshot.forEach((doc) => {
let newPictureData = doc.data();
newPictureData.id = doc.id;
newPicturesData.push(newPictureData);
});
this.picturesData = newPicturesData; // this should overwrite the data in my state, right? But instead it's appending.
});
},
It's difficult to tell you exactly what's happening without thoroughly testing your code but you have to note that the two calls (to getImages() and refreshImages()) may not be done in the order you expect.
Since in getImages() you push the data to picturesData and in refreshImages() you replace picturesData, I suspect that the listener set through refreshImages() returns data before you get the result of the query triggered by getImages().
Actually, since onSnapshot() triggers an initial call that returns the entire result of the query, you only need to call refreshImages() (you don't need the initial call to getImages()).
Note that onSnapshot() is not an asynchronous method like get(), so you don't need to make refreshImages() async.
okay so everyone knows a separate mapping will do the trick of rendering a component in a React render method, something like this:
var listOfService = ['A', 'B', 'C', 'D', 'E'];
const listItems = listOfService.map((singleItem) =>
<a className="style_class">{singleItem}</a>
);
and if you call listItems in the return of a render method, you will get the list and thats for a single list but I have a firebase firestore database and I want to loop through that database and get each document printed as a React Component I have called Service, I know that you can't use JSX inside a loop or if statement so I tried this:
renderServices() {
let db = firebase.firestore();
var details = [[]];
db.collection('providers').get().then(function (snapshot) {
snapshot.forEach(function (doc) {
details.push(
[
doc.data().owner_name,
doc.data().experience,
doc.data().charges,
doc.data().address,
doc.data().home_service,
doc.data().highlights,
doc.data().certifications,
doc.data().specialty,
doc.data().facility
]
);
//tried loading here Serivce, didn't work
});
})
.catch(function (error) {
console.log(error);
});
details.map((singleDetail) =>
(< Service details={singleDetail} />)
);
}
so I tried this but its not working, I wrote some console logs and the data is coming in correctly. First I get the whole array of "providers" documents, that providers list gets passed on as "snapshot" so I loop through the snapshot with another function and that function is taking a single document called doc and then pushing it in the var called details, which is an array of arrays but! its not working, the array is not being populated even tho the data is there and therefore I am unable to map, any idea on how to approach this?
You need to separate your loading of data and the rendering of it because retrieving the data is asynchronous and rendering needs to be synchronous.
For example, in your case you are pushing the details into the array in the then callback - this will happen async to your main function so when you call details.map it is still empty (the callback hasn't fired to populate it).
Instead, you can load the data and store it in the state and then render it if it is available. Personally I prefer async/await but the same logic can be achieved with using then on promises:
const [details, setDetails] = useState([]);
// Load the data on initial load or setup the effect to fire when reload is needed
useEffect(() => {
// Async function that loads the data and sets the state once ready
const loadData = async () => {
const details = [];
try {
const snapshot = await db.collection('providers').get();
snapshot.forEach((doc) => {
details.push([
// your details stuff
]);
}
} catch (ex) {
// Handle any loading exceptions
}
setDetails(details);
}
}, [])
// Render the available details, you could also add conditional rendering to check
// if the details are available or not yet
return (
<div>
{details.map((singleDetail) => (
<Service details={singleDetail} />
)};
</div>
);
If you are using React classes instead of functional components, you would call the loading from componentDidMount and still set the state after it is loaded, then map over the state data when rendering.
I have found the solution, using ReactDOM.render method
first make a global array:
var Services = [];
and then add a DOM element where the array can be placed and rendered:
<div className="row d-flex justify-content-center" id='service-div'>
</div>
once you have the DOM element in place, let the page load, when the load completes, ComponentDidMount() is called, so put your method in there so its
ComponentDidMount(){
this.renderServices();}
now in this method, just populate the array and then use ReactDOM.render method to render it to a given DOM element
async renderServices() {
let history = this.props.history; //in case the method can't access the history
let db = firebase.firestore();
selectedServices = this.props.location.state.selectedServices;
var database = firebase.database();
db.collection('providers').where('services', 'array-contains-any',
selectedServices).get()
.then(function (snapshot) {
let idCount = 0;
snapshot.forEach(function (doc) {
let tempDetails = [];
tempDetails.push(
doc.data().owner_name,
doc.data().experience,
doc.data().charges,
doc.data().address,
doc.data().home_service,
doc.data().highlights,
doc.data().certifications,
doc.data().specialty,
doc.data().facility,
doc.data().services,
doc.data().call_extension,
idCount.toString()
);
Services.push(<Service details={tempDetails} history={history} />); //adding the Service Component into a list
idCount++;
});
ReactDOM.render(<div>{Services}</div>, document.getElementById('service-div')); //now rendering the entire list into the div
}).catch(function (error) {
console.log(error);
});
}
Why is the state not updating? I have checked that i receive data from the api in the correct form. I can print the result if i change it instead of trying to add it to a new array and put it in the state. I have also checked that this exists in the scope and as I understand it should work since I am using an arrow function. I still receive an empty array after the map function.
What am I missing?
class CurrencyForm extends React.Component {
state = {
currencies: [],
}
componentDidMount(){
this.fetchCurrencies();
}
fetchCurrencies = async () => {
const response = await axios.get('some address')
response.data.map(currency =>
this.setState({currencies: [...this.state.currencies,currency.id]}
))
}
The problem is that you are using [...this.state.currencies, currency.id].
Since setState is also async, the state does not change for each iteration of the map. So you are always using the same this.state.currencies. This means that you should only get the last currency.id added.
You should either use the function version of setState
this.setState((state) => ({currencies: [...state.currencies,currency.id]}));
or simply do the map and use the resulting array to set the state
fetchCurrencies = async () => {
const response = await axios.get('some address');
const currencyIds = response.data.map(currency=>currency.id);
this.setState({currencies: [...this.state.currencies,...currencyIds]}
}
if you want to put all the id's in the state.. instead of calling setState so many times, You could put all the ids in an array first then update the state with that array something like this:
fetchCurrencies = async () => {
const response = await axios.get('some address')
const idsArray = response.data.map(currency => currency.id)
this.setState({currencies: idsArray})
}
and remember setState is an async call so you may not be able to see the result if you put console.log just after setState instead try console logging in your render method
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
}
});
}
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