I'm developing the front-end for my spring boot application. I set up an initial call wrapped in a useEffect() React.js function:
useEffect(() => {
const getData = async () => {
try {
const { data } = await fetchContext.authAxios.get(
'/myapi/' + auth.authState.id
);
setData(data);
} catch (err) {
console.log(err);
}
};
getData();
}, [fetchContext]);
The data returned isn't comprehensive, and needs further call to retrieve other piece of information, for example this initial call return an employee id, but if I want to retrieve his name and display it I need a sub-sequential call, and here I'm experiencing tons of issues:
const getEmployeeName = async id => {
try {
const name = await fetchContext.authAxios.get(
'/employeeName/' + id
);
console.log((name["data"])); // <= Correctly display the name
return name["data"]; // return an [Object promise],
} catch (err) {
console.log(err);
}
};
I tried to wrap the return call inside a Promise.resolve() function, but didn't solve the problem. Upon reading to similar questions here on stackoverflow, most of the answers suggested to create a callback function or use the await keyword (as I've done), but unfortunately didn't solve the issue. I admit that this may not be the most elegant way to do it, as I'm still learning JS/React I'm open to suggestions on how to improve the api calls.
var output = Object.values(data).map((index) =>
<Appointment
key={index["storeID"].toString()}
// other irrelevant props
employee={name}
approved={index["approved"]}
/>);
return output;
Async functions always return promises. Any code that needs to interact with the value needs to either call .then on the promise, or be in an async function and await the promise.
In your case, you should just need to move your code into the existing useEffect, and setState when you're done. I'm assuming that the employeeID is part of the data returned by the first fetch:
const [name, setName] = useState('');
useEffect(() => {
const getData = async () => {
try {
const { data } = await fetchContext.authAxios.get(
"/myapi/" + auth.authState.id
);
setData(data);
const name = await fetchContext.authAxios.get(
'/employeeName/' + data.employeeID
);
setName(name.data);
} catch (err) {
console.log(err);
}
};
getData();
}, [fetchContext]);
// ...
var output = Object.values(appointmentsData).map((index) =>
<Appointment
key={index["storeID"].toString()}
// other irrelevant props
employee={name}
approved={index["approved"]}
/>);
return output;
Note that the above code will do a rerender once it has the data (but no name), and another later when you have the name. If you want to wait until both fetches are complete, simply move the setData(data) down next to the setName
Related
I'm trying to access what's returned by secondSnapshot.data(), but am having an issue, as described by the comments below. I tried to create an async function, but to no avail. Any idea what's going wrong? Please view the 2 comments.
useEffect(() => {
firestore.collection(`comments`).onSnapshot((snapshot) => {
const posts = snapshot.docs
.map((doc) => {
const address = doc.data().comments?.map((comment) => {
comment.get().then((secondSnapshot) => {
console.log("snapshot", secondSnapshot.data());
#I SEE WHAT I EXPECT TO SEE
return secondSnapshot.data();
});
});
console.log(address) #THIS RETURNS UNDEFINED FOR SOME REASON??
return {
username: doc.data().username,
date: doc.data().date.seconds,
text: doc.data().text,
votes: doc.data().votes,
comments: [],
};
});
props.setComments(posts);
});
}, [location]);
Besides the React vs Firebase design problem pointed by #Mulan, your code has several issues.
Issue #1
The following map function returns nothing. You won't find anything in address if you don't return something from this block.
const address = doc.data().comments?.map((comment) => {
/* This MUST return something */
});
Issue #2
You are mixing synchronous and asynchronous code. Basically, your attempt to print address value before the different async calls to comment.get() finished running.
const address = doc.data().comments?.map((comment) => {
/* async code inside */
});
console.log(address); // this runs without waiting for the async code
Advice
If you are having a hard time with the old promise syntax, try using async / await instead:
firestore.collection(`comments`).onSnapshot(async (snapshot) => {
const postPromises = snapshot.docs
.map(async (doc) => {
const comments = doc.data().comments ?? []; // assign an empty array if there are no comments
const secondSnapshots = await Promise.all(comments.map((comment) => comment.get()));
const addresses = secondSnapshots.map((secondSnapshot) => secondSnapshot.data());
console.log(addresses);
return { /* ... */ };
});
const posts = await Promise.all(postPromises);
props.setComments(posts);
});
I'm not sure you should be calling comment.get() inside onSnapshot, but this is another story and I'm not a Firebase specialist.
I have two tables in Firebase: Vouchers & ClaimedVouchers. I am trying to display the vouchers that do not appear in the ClaimedVouchers table. So, I have a query that gets all of the vouchers, then another that checks if they're claimed and if it's claimed or not the function should return either true or false:
This is isClaimedAPI.js
export default () => {
var result;
async function isClaimed(voucher, user) {
var db = Firebase.firestore();
console.log("voucher");
await db
.collection("ClaimedVoucher")
.where("voucherID", "==", voucher)
.where("userID", "==", user)
.get()
.then(function (querySnapshot) {
if (querySnapshot.empty === false) {
result = true;
} else {
result = false;
}
})
.catch(function (error) {
console.log("Error getting documents: ", error);
});
console.log(result, "this is result");
return result;
//Call when component is rendered
useEffect(() => {
isClaimed().then((result) => setResult(result));
}, []);
return [isClaimed];
And then in my main function:
var user = Firebase.auth().currentUser;
var uid = user.uid;
const [getVouchers, voucherList, errorMessage] = getVouchersAPI(ID); //List of vouchers to be checked
const [isClaimed] = isClaimedAPI();
return(
<ScrollView>
{voucherList.map((item, index) => {
var voucher = item;
var isVoucherClaimed = isClaimed(voucher.voucherID, uid);
console.log("this is result, ", isVoucherClaimed);
if (
isVoucherClaimed === false
) {
return <Text>{item.name}<Text>
}
})}
</ScrollView>
);
Now nothing happens and I receive the following warning: [Unhandled promise rejection: FirebaseError: Function Query.where() requires a valid third argument, but it was undefined.] but I think this is unrelated to the issue.
Your isClaimed is an async function, meaning that it returns a promise - or a delayed result. If you want to wait for the result when calling isClaimed, you'll need to use await:
await isClaimed(voucher.voucherID, uid);
console.log(result);
This most likely isn't possible in a render method though, which is why (as Asutosh commented) you'll have to store the result in the state, and then use the state in your render method.
So the setup you need is:
Start the loading of all your data in componentDidMount or with useEffect.
When the data is loaded, put it in the state with setState or a state hook.
Use the data in your render method.
For a few examples of this, see:
Firebase switch header option with onAuthStateChanged
React + Firestore : Return a variable from a query
How to render async data in react + firestore?
Just to highlight how you can use isClaimed as hoook and set state calling a async function inside it. Later use the above hook in a react component. Please follow below sanbox.
https://codesandbox.io/s/confident-cray-4g2md?file=/src/App.js
I have two tables in Firebase: Vouchers & ClaimedVouchers. I am trying to display the vouchers that do not appear in the ClaimedVouchers table. So, I have a query that gets all of the vouchers, then another that checks if they're claimed and if it's claimed or not the function should return either true or false:
This is isClaimedAPI.js
export default () => {
var result;
async function isClaimed(voucher, user) {
var db = Firebase.firestore();
console.log("voucher");
await db
.collection("ClaimedVoucher")
.where("voucherID", "==", voucher)
.where("userID", "==", user)
.get()
.then(function (querySnapshot) {
if (querySnapshot.empty === false) {
result = true;
} else {
result = false;
}
})
.catch(function (error) {
console.log("Error getting documents: ", error);
});
console.log(result, "this is result");
return result;
//Call when component is rendered
useEffect(() => {
isClaimed().then((result) => setResult(result));
}, []);
return [isClaimed];
And then in my main function:
var user = Firebase.auth().currentUser;
var uid = user.uid;
const [getVouchers, voucherList, errorMessage] = getVouchersAPI(ID); //List of vouchers to be checked
const [isClaimed] = isClaimedAPI();
return(
<ScrollView>
{voucherList.map((item, index) => {
var voucher = item;
var isVoucherClaimed = isClaimed(voucher.voucherID, uid);
console.log("this is result, ", isVoucherClaimed);
if (
isVoucherClaimed === false
) {
return <Text>{item.name}<Text>
}
})}
</ScrollView>
);
Now nothing happens and I receive the following warning: [Unhandled promise rejection: FirebaseError: Function Query.where() requires a valid third argument, but it was undefined.] but I think this is unrelated to the issue.
Your isClaimed is an async function, meaning that it returns a promise - or a delayed result. If you want to wait for the result when calling isClaimed, you'll need to use await:
await isClaimed(voucher.voucherID, uid);
console.log(result);
This most likely isn't possible in a render method though, which is why (as Asutosh commented) you'll have to store the result in the state, and then use the state in your render method.
So the setup you need is:
Start the loading of all your data in componentDidMount or with useEffect.
When the data is loaded, put it in the state with setState or a state hook.
Use the data in your render method.
For a few examples of this, see:
Firebase switch header option with onAuthStateChanged
React + Firestore : Return a variable from a query
How to render async data in react + firestore?
Just to highlight how you can use isClaimed as hoook and set state calling a async function inside it. Later use the above hook in a react component. Please follow below sanbox.
https://codesandbox.io/s/confident-cray-4g2md?file=/src/App.js
I have a function which fetch a data from Firestore :
getLastTime(collectionName: string) {
const docRef = this.afs.firestore.collection(collectionName).doc(this.User).collection('lastTime').doc('lastTime');
docRef.get().then(doc => {
if (doc.exists) {
this.get = doc.data().lastTime;
} else {
this.get = 'Never done';
}
}).catch(error => {
console.log('Error getting document:', error);
});
return this.get;
}
For my test I actually have a string value inside the doc 'lastTime' which is a string.
Inside ngOnInit(), I called my function and console.log the result
this.InjuredLastTime = this.getLastTime('INJURY');
console.log(this. this.InjuredLastTime);
Normally I should have my string printed inside the console but I got undefined...
Maybe it's because Firestore do not fetch fast enough my data, but I am quiet surprised since Firestore is quiet fast normally...
You don't actually wait for the promise that is created by docRef.get() before you return from getLastTime(). So, unless the call to firebase is instant (e.g. never) this won't work.
The correct solution really depends on what you are doing with this.InjuredLastTime. But one approach would just be to return a promise and set it after it is ready:
getLastTime(collectionName: string) {
const docRef = this.afs.firestore.collection(collectionName).doc(this.User).collection('lastTime').doc('lastTime');
return docRef.get().then(doc => {
if (doc.exists) {
return doc.data().lastTime;
} else {
return 'Never done';
}
}).catch(error => {
console.log('Error getting document:', error);
return null;
});
}
Then, instead of the assignment synchronously, do it asynchronously:
this.getLastTime('INJURY').then(result => { this.InjuredLastTime = result });
Data is loaded from Firestore asynchronously, since it may take some time before the data comes back from the server. To prevent having to block the browser, your code is instead allowed to continue to run, and then your callback is called when the data is available.
You can easily see this with a few well-placed log statements:
const docRef = this.afs.firestore.collection(collectionName).doc(this.User).collection('lastTime').doc('lastTime');
console.log('Before starting to get data');
docRef.get().then(doc => {
console.log('Got data');
});
console.log('After starting to get data');
If you run this code, you'll get:
Before starting to get data
After starting to get data
Got data
This is probably not the order that you expected the logging output in, but it is actually the correct behavior. And it completely explains why you're getting undefined out of your getLastTime function: by the time return this.get; runs, the data hasn't loaded yet.
The simplest solution in modern JavaScript is to mark your function as async and then await its result. That would look something like this:
async function getLastTime(collectionName: string) {
const docRef = this.afs.firestore.collection(collectionName).doc(this.User).collection('lastTime').doc('lastTime');
doc = await docRef.get();
if (doc.exists) {
this.get = doc.data().lastTime;
} else {
this.get = 'Never done';
}
return this.get;
}
And then call it with:
this.InjuredLastTime = await this.getLastTime('INJURY');
In Node.js, I have a Promise.all(array) resolution with a resulting value that I need to combine with the results of another asynchronous function call. I am having problems getting the results of this second function, since it resolves later than the promise.all. I could add it to the Promise.all, but it would ruin my algorithm. Is there a way to get these values outside of their resolutions so I can modify them statically? Can I create a container that waits for their results?
To be more specific, I am reading from a Firebase realtime database that has been polling API data. I need to run an algorithm on this data and store it in a MongoDB archive. But the archive opens aynchronously and I can't get it to open before my results resolve (which need to be written).
Example:
module.exports = {
news: async function getNews() {
try {
const response = await axios.get('https://cryptopanic.com/api/posts/?auth_token=518dacbc2f54788fcbd9e182521851725a09b4fa&public=true');
//console.log(response.data.results);
var news = [];
response.data.results.forEach((results) => {
news.push(results.title);
news.push(results.published_at);
news.push(results.url);
});
console.log(news);
return news;
} catch (error) {
console.error(error);
}
},
coins: async function resolution() {
await Promise.all(firebasePromise).then((values) => {
//code
return value
}
}
I have tried the first solution, and it works for the first entry, but I may be writing my async function wrong on my second export, because it returns undefined.
You can return a Promise from getNews
module.exports = {
news: function getNews() {
return axios.get('https://cryptopanic.com/api/posts/?auth_token=518dacbc2f54788fcbd9e182521851725a09b4fa&public=true')
.then(res => {
// Do your stuff
return res;
})
}
}
and then
let promiseSlowest = news();
let promiseOther1 = willResolveSoon1();
let promiseOther2 = willResolveSoon2();
let promiseOther3 = willResolveSoon3();
Promise.all([ promiseOther1, promiseOther2, promiseOther3 ]).then([data1,
data2, data3] => {
promiseSlowest.then(lastData => {
// data1, data2, data3, lastData all will be defined here
})
})
The benefit here is all promises will start and run concurrently so your total waiting time will be equal to the time taken by the promiseSlowest.
Check the link for more:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function#Examples