Situation
I need to form a comments timeline which looks like this,
Yet the information comes from 2 different APIs,
API Format
1. Comments API, an array of comments by chronological order
[
{
comment: 'Hello, how's your day?',
userId: '001',
timestamp: '1670548338131'
},
{
comment: 'Pretty good!',
userId: '002',
timestamp: '1670548338151'
},
{
comment: 'Want to hang out later?',
userId: '001',
timestamp: '1670548338171'
},
...
]
2. User info API, 1 user info per search
{
userId: '001',
userName: 'Ben',
userProfileUrl: 'https://www.photo.com/001'
}
Questions
What's the better way to call API 2 mutiple times?
What's the better way to form the render array? (Want the highest efficiency)
Although modifying the backend data model would be ideal, I'm under some constraints that I can only manipulate data on the frontend.
Initial Idea
Fetch comments list first, use a Set to collect unique userIds
Use Promise.all to fetch API 2 parallelly with all the userIds within the Set
Form a dictionary (map) to lookup user info by userId
Iterate the list from step 1, fill in user info one by one, and produce a new list
Set the final result list into useState state
Render
const getComments = () => axios.get('API_1')
const getUser = (userId) => axios.get(`API_2/${userId}`)
const [renderList, setRenderList] = React.useState([])
const initComments = async () => {
try {
const res = await getComments()
const rawComments = res.data
const usersSet = new Set(rawComments.map(c => c.userId))
const promises = [...usersSet].map(u => getUser(u))
// Here's the question 1, is it viable?
const usersInfo = await Promise.all(promises)
const usersMap = usersInfo.reduce((prev, curr) => {
return {
...prev,
[curr.data.userId]: curr.data,
}
}, {})
// Here's the question 2, is it the most efficient way?
const finalList = rawComments.map(c => {
return {
...c,
userName: usersMap[c.userId].userName,
userProfileUrl: usersMap[c.userId].userProfileUrl,
}
})
setRenderList(finalList)
} catch(err) {}
}
React.useEffect(() => {
initComments()
}, [])
return renderList.map(item => <>{/* Do the render here */}</>)
I'm trying to run two sequencitally graphQL requests, the first one give me data that I need into the second one parameters. And I don't know how to wait to the first.
My program is the following one:
I have the declaration of my GraphQL requests:
const [
addConfigurableProductToCart,
{ error: errorAddingSimpleProduct, loading: isAddSimpleLoading }
] = useMutation(ADD_CONFIGURABLE_MUTATION);
const [getDataParentSku, { error, loading, data }] = useLazyQuery(
GET_PARENT_SKU
);
And the main workflow are in this function.
const handleAddProductsToCart = useCallback(
async csvProducts => {
let tempSkuErrorList = [];
for (let i = 0; i < csvProducts.length; i++) {
const orParentSku = getDataVariable(csvProducts[i][0]);
const variables = {
cartId,
quantity: parseInt(csvProducts[i][1], 10),
sku: csvProducts[i][0],
parentSku: orParentSku.then(res => {
return res.products.items[0].orParentSku;
})
};
try {
await addConfigurableProductToCart({
variables
});
} catch {
tempSkuErrorList.push(csvProducts[i][0]);
}
}
},
[
addConfigurableProductToCart,
cartId,
]
);
getDataVariable() is the function who call the first query (useLazyQuery()). And its content is:
const getDataVariable = useCallback(
async sku => {
getDataParentSku({
variables: { sku: sku }
});
return await data;
},
[getDataParentSku, data]
);
The error that I have been finding all the time is that when I need the data, is undefined.
Another option was the idea of using this library https://www.npmjs.com/package/graphql-lodash, in order to merge the query into one, but is outdated.
Thanks a lot for your help.
It seems im using async wrong, can anybody spot what I am doing wrong?
This is the function I am waiting on:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export async function firebaseAcceptTradeOffer(tradeOfferID, userData) {
var tradeInstanceID;
var senderID;
var receiverID;
var senderItemsTemp;
var receiverItemsTemp;
var response;
var tradeOffer = db.collection("tradeOffers").doc(tradeOfferID);
return tradeOffer
.get()
.then((doc) => {
senderItemsTemp = doc.data().sendersItems;
receiverItemsTemp = doc.data().receiversItems;
senderID = doc.data().senderID;
receiverID = doc.data().receiverID;
})
.then(() => {
var itemInTrade = false;
senderItemsTemp.forEach((item) => {
db.collection("listings")
.doc(item.itemID)
.get()
.then((doc) => {
if (doc.data().status !== "listed") {
itemInTrade = true;
}
})
.then(() => {
receiverItemsTemp.forEach((item) => {
db.collection("listings")
.doc(item.itemID)
.get()
.then((doc) => {
if (doc.data().status !== "listed") {
itemInTrade = true;
}
})
.then(() => {
if (itemInTrade) {
tradeOffer.update({
status: "declined",
});
return false;
} else {
db.collection("trades")
.add({
tradeOfferID: tradeOfferID,
senderTradeStatus: {
created: true,
sentToSeekio: "current",
inspection: false,
sentToPartner: false,
},
receiverTradeStatus: {
created: true,
sentToSeekio: "current",
inspection: false,
sentToPartner: false,
},
postagePhotos: [],
inspectionPhotos: [],
senderPaid: false,
receiverPaid: false,
senderUploadedProof: false,
receiverUploadedProof: false,
senderID: senderID,
receiverID: receiverID,
messages: [
{
message: `Trade created. A representative, will message this chat shortly with instructions and postage address. If you would like more information about the trading process, head to seekio.io/help. Thank you for using Seekio!`,
sender: "System",
timestamp: firebase.firestore.Timestamp.fromDate(
new Date()
),
},
],
})
.then((docRef) => {
tradeInstanceID = docRef.id;
tradeOffer
.set(
{
status: "accepted",
tradeInstanceID: docRef.id,
},
{ merge: true }
)
.then(() => {
var receiver = db.collection("users").doc(senderID);
var notification = {
from: auth.currentUser.uid,
fromUsername: userData.username,
type: "tradeOfferAccepted",
time: firebase.firestore.Timestamp.fromDate(
new Date()
),
seen: false,
};
receiver
.update({
notifications: firebase.firestore.FieldValue.arrayUnion(
notification
),
})
.then(() => {
response = {
sendersItems: senderItemsTemp,
receiversItems: receiverItemsTemp,
};
return response;
});
});
})
.catch((err) => console.log(err));
}
});
});
});
});
});
}
And here is where I am calling it:
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
async function acceptTradeOffer() {
var tradeOfferID = currentTradeFocus;
var senderID = "";
setLoading("loading");
if (userData !== null && tradeOfferID !== "") {
const response = await firebaseAcceptTradeOffer(
currentTradeFocus,
userData
);
console.log(
"RESPONSE FROM FIREBASE SERVICE>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>: ",
response
);
if (!response) {
setErrorMsg("One of the selected items is no longer available.");
} else if (
response.sendersItems !== null &&
response.receiversItems !== null
) {
setSenderItems(response.sendersItems);
setReceiverItems(response.receiversItems);
toggleConfirmScreen("cancel");
setLoading("idle");
setItemsSet(true);
}
fetch(
"https://europe-west2-seekio-86408.cloudfunctions.net/sendMail?type=tradeUpdate&userID=" +
senderID
).catch((err) => {
console.log(err);
setLoading("idle");
});
}
}
So basically I want to go and check if any of the items in this 'trade' are not equal to 'listed' (which means they are not available, I want to return false, if not, then I return the array of items so the trade can continue.
EDIT: I've tried to rejig it all and it's half working. A top level look at what I am trying to do:
User wants to accept a trade offer for some items >
Check through all items to make sure they are available and not sold >
If so, accept the trade >
Then once its accepted, go and cancel all remaining trade offers that include items from this accepted trade, cause they are not available anymore.
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export async function firebaseAcceptTradeOffer(tradeOfferID, userData) {
console.log(
"----- starting firebaseAcceptTradeOffer--------- ",
unavailableItem
);
//==============
var tradeInstanceID;
var senderID;
var receiverID;
var senderItemsTemp;
var receiverItemsTemp;
var unavailableItem = false;
var response;
var itemsArray;
var notListed = false;
//==============
var tradeOffer = db.collection("tradeOffers").doc(tradeOfferID);
unavailableItem = tradeOffer
.get()
.then((doc) => {
senderID = doc.data().senderID;
receiverID = doc.data().receiverID;
itemsArray = doc.data().sendersItems.concat(doc.data().receiversItems);
})
.then(() => {
itemsArray.forEach((item) => {
db.collection("listings")
.doc(item.itemID)
.get()
.then((doc) => {
if (doc.data().status !== "listed") {
notListed = true;
}
});
});
})
.then(() => {
return notListed;
});
console.log(
"-----unavailableItem at the end of method --------- ",
unavailableItem
);
//^^^^^^^^^^ here i am getting a promise result of false (which is correct) but HOW CAN I ACCESS IT
if (unavailableItem) {
tradeOffer.update({
status: "declined",
});
return false;
} else {
response = await createTrade(
tradeOffer,
tradeOfferID,
senderID,
receiverID,
userData.username
);
console.log("response from createTrade", response);
return response;
}
}
I am getting a promise object back with the value false above. False is correct value I am expecting, but how can I access it? its in the form of a promise object?
I have some time on my hands, so let's break this down.
Notes on Variables
If you aren't using TypeScript (and even if you are), I highly recommend inserting the type into the name of your variables.
db # ✔ by convention, either firebase.database() or firebase.firestore()
tradeOffer # ❓ type unclear, could be a number, an object, a string, etc
tradeOfferDocRef # ✔ a DocumentReference
trades # ❓ type unclear, plural implies a collection of some sort
tradesColRef # ✔ a CollectionReference
You may also encounter these:
doc # ❓ by convention, a DocumentSnapshot, but with unknown data
tradeDoc # ✔ implies a DocumentSnapshot<TradeData> (DocumentSnapshot containing trade data)
When using just doc, you need to look around where its used for context on what this DocumentSnapshot contains.
db.collection('trades').doc(tradeOfferID).get()
.then((doc) => { // contents implied to be TradeData
const data = doc.data();
});
// or
tradeDocRef.get()
.then((doc) => { // contents implied to be TradeData
const data = doc.data();
});
You should rename doc as appropriate, especially when using async/await syntax, so you don't end up in situations like:
const doc = await db.collection('trades').doc(tradeOfferID).get();
/* ... many lines ... */
const senderID = doc.get("senderID"); // what was doc again?
As you've tagged reactjs in your question, this implies you are using modern JavaScript.
Ditch any use of var and replace it with the block-scoped versions: const (prevents reassigning the variable) or let (similar to var, but not quite). These are safer and prevents the chances of accidentally overwriting something you shouldn't.
You can also make use of Object destructuring to assign your variables.
const senderID = doc.data().senderID;
const receiverID = doc.data().receiverID;
const itemsArray = doc.data().sendersItems.concat(doc.data().receiversItems);
can become:
const { senderID, receiverID, sendersItems, receiversItems } = doc.data();
const itemsArray = sendersItems.concat(receiversItems);
If you ever need only a single property out of a document, you should use DocumentSnapshot#get() instead of DocumentSnapshot#data() so it will parse only the field you want instead of the whole document's data.
function getUserAddress(uid) {
return firebase.firestore()
.collection('users')
.doc(uid)
.get()
.then(userDoc => userDoc.get("address")); // skips username, email, phone, etc
}
Notes on Promises
var senderID;
var receiverID;
var itemsArray;
tradeOfferDocRef
.get()
.then((doc) => {
senderID = doc.data().senderID;
receiverID = doc.data().receiverID;
itemsArray = doc.data().sendersItems.concat(doc.data().receiversItems);
})
.then(() => {
/* use results from above */
});
While the above code block functions as intended, when you have many of these variables like this as you do, it becomes unclear when and where they are set.
It also leads to problems like this where you think the variable has a value:
var senderID;
var receiverID;
var itemsArray;
tradeOfferDocRef
.get()
.then((doc) => {
// this line runs after the line below
senderID = doc.data().senderID;
receiverID = doc.data().receiverID;
itemsArray = doc.data().sendersItems.concat(doc.data().receiversItems);
});
// this line before the line above
console.log(senderID); // will always log "undefined"
This can be avoided in one of three ways:
Returning data to pass through to the next handler (you wouldn't use this for this example, only if the next then() handler is elsewhere):
tradeOfferDocRef
.get()
.then((doc) => {
const { senderID, receiverID, sendersItems, receiversItems } = doc.data();
const itemsArray = sendersItems.concat(receiversItems);
return { senderID, receiverID, itemsArray }; // pass to next step
})
.then((neededData) =>
/* use neededData.senderID, neededData.receiverID, etc */
});
Using the data within the same handler:
tradeOfferDocRef
.get()
.then((doc) => {
const { senderID, receiverID, sendersItems, receiversItems } = doc.data();
const itemsArray = sendersItems.concat(receiversItems);
/* use results from above */
});
Using async-await syntax:
const tradeDoc = await tradeOfferDocRef.get();
const { senderID, receiverID, sendersItems, receiversItems } = tradeDoc.data();
const itemsArray = sendersItems.concat(receiversItems);
/* use results from above */
Writing to Firestore
Your current code consists of the following steps:
1. Get the trade offer document</li>
2. If successful, pull out the sender and receiver's IDs, along with any items in the trade
3. If successful, do the following for each item in the sender items array:
a) Check if any of the sender's items are unavailable</li>
b) If successful, do the following for each item in the receiver items array:
- If **any item** was unavailable prior to this, decline the trade & return `false`.
- If all items **so far** are available, do the following:
a) Create a document containing information about the trade with the needed data
b) If successful, edit the trade offer document to accept it
c) If successful, create a notification for the receiver
d) If successful, return the traded items
e) If any of a) to d) fail, log the error and return `undefined` instead
4. Return `undefined`
In the above steps, you can see some problems with your promise chaining. But aside from that, you can also see that you create and edit documents one-by-one instead of all-at-once ("atomically"). If any of these writes were to fail, your database ends up in an unknown state. As an example, you could have created and accepted a trade, but failed to create the notification.
To atomically write to your database, you need to use a batched write where you bundle a bunch of changes together and then send them off to Firestore. If any of them were to fail, no data is changed in the database.
Next, you store a user's notifications inside of their user document. For a small number of notifications this is fine, but do you need to download all of those notifications if you wanted to pull just an address or phone number like in the example in the above section? I recommend splitting them out into their own document (such as /users/{someUserId}/metadata/notifications), but ideally their own collection (such as /users/{someUserId}/notifications/{someNotificationID}). By placing them in their own collection, you can query them and use QuerySnapshot#docChanges to synchronize changes and use Cloud Firestore triggers to send push notifications.
Refactored Function
1. Get the trade offer document</li>
2. Once the retrieved, do the following depending on the result:
- If failed or empty, return an error
- If successful, do the following:
a) Pull out the sender and receiver's IDs, along with any items in the trade.
b) For each item in the trade, check if any are unavailable and once the check has completed, do the following depending on the result:
- If any item is unavailable, do the following:
a) Decline the trade
b) Return the list of unavailable items
- If all items are available, do the following:
a) Create a new write batch containing:
- Create a document about the trade
- Edit the trade offer document to accept it
- Create a notification for the receiver
b) Commit the write batch to Firestore
c) Once the commit has completed, do the following depending on the result:
- If failed, return an error
- If successful, return the traded items and the trade's ID
Because the steps here depend on each other, this is a good candidate to use async/await syntax.
To see this in action, closely study this:
import * as firebase from "firebase-admin";
// insert here: https://gist.github.com/samthecodingman/aea3bc9481bbab0a7fbc72069940e527
async function firebaseAcceptTradeOffer(tradeOfferID, userData) {
const tradeOfferDocRef = db.collection("tradeOffers").doc(tradeOfferID);
const tradeDoc = await tradeOfferDocRef.get();
const { senderID, receiverID, sendersItems, receiversItems } =
tradeDoc.data();
const itemsArray = sendersItems.concat(receiversItems);
// TODO: Check if this is an accurate assumption
if (sendersItems.length == 0 || receiversItems.length == 0) {
success: false,
message: "One-sided trades are not permitted",
detail: {
sendersItemsIDs: sendersItems.map(({ itemID }) => itemID),
receiversItemsIDs: receiversItems.map(({ itemID }) => itemID),
},
};
const listingsColQuery = db
.collection("listings")
.where("status", "==", "listed");
const uniqueItemIds = Array.from(
itemsArray.reduce(
(set, { itemID }) => set.add(itemID),
new Set()
)
);
const foundIds = {};
await fetchDocumentsWithId(
listingsColQuery,
uniqueItemIds,
(listingDoc) => {
// if here, listingDoc must exist because we used .where("status") above
foundIds[listingDoc.id] = true;
}
);
const unavailableItemIDs = uniqueItemIds
.filter(id => !foundIds[id]);
if (unavailableItems.length > 0) {
// one or more items are unavailable!
await tradeOfferDocRef.update({
status: "declined",
});
return {
success: false,
message: "Some items were unavailable",
detail: {
unavailableItemIDs,
},
};
}
const tradeDocRef = db.collection("trades").doc();
const tradeInstanceID = tradeDocRef.id;
const batch = db.batch();
batch.set(tradeDocRef, {
tradeOfferID,
senderTradeStatus: {
created: true,
sentToSeekio: "current",
inspection: false,
sentToPartner: false,
},
receiverTradeStatus: {
created: true,
sentToSeekio: "current",
inspection: false,
sentToPartner: false,
},
postagePhotos: [],
inspectionPhotos: [],
senderPaid: false,
receiverPaid: false,
senderUploadedProof: false,
receiverUploadedProof: false,
senderID,
receiverID,
messages: [
{
message: `Trade created. A representative, will message this chat shortly with instructions and postage address. If you would like more information about the trading process, head to seekio.io/help. Thank you for using Seekio!`,
sender: "System",
timestamp: firebase.firestore.Timestamp.fromDate(new Date()),
},
],
});
batch.set(
tradeOfferDocRef,
{
status: "accepted",
tradeInstanceID,
},
{ merge: true }
);
const receiverNotificationRef = db
.collection("users")
.doc(senderID)
.collection("notifications")
.doc();
batch.set(receiverNotificationRef, {
from: auth.currentUser.uid,
fromUsername: userData.username,
type: "tradeOfferAccepted",
time: firebase.firestore.Timestamp.fromDate(new Date()),
seen: false,
});
await batch.commit();
return {
success: true,
message: "Trade accepted",
detail: {
tradeID: tradeInstanceID,
senderItems,
receiversItems,
},
};
}
Usage:
try {
const tradeResult = await firebaseAcceptTradeOffer(someTradeId);
} catch (err) {
// if here, one of the following things happened:
// - syntax error
// - database read/write error
// - database rejected batch write
}
In general, when you are returning a promise where it can't be resolved you must await its result. Additionally, you must be returning a value from within a promise then chain, at minimal the last .then() needs to be returning a value, this can also be done within a .finally() method.
Using Get from any firebase resource, realtime, firestore, and storage are all Async processes and must be awaited. in your case, you are missing an await for the return:
var tradeOffer = db.collection("tradeOffers").doc(tradeOfferID);
return tradeOffer
and you don't appear to be returning anything inside your .then() statements, I would suggest a complete rewrite of what you are trying to so you are returning values as they are needed.
Hi I'm currently blocked because I can't get all records from a collection with references values.
I would like to get all records from collection events (it works) but when I wanna merge the category information associated with categoryId my code doesn't work anymore.
Events collection
Categories collection
export const getEventsRequest = async () => {
const output = [];
const data = await firebase.firestore().collection('events').get();
data.forEach(async (doc) => {
const {
name,
address,
city,
duration,
level,
startDate,
maxPeople,
categoryId,
} = doc.data();
const { name: categoryName, color } = (
await firebase.firestore().collection('categories').doc(categoryId).get()
).data();
output.push({
name,
address,
city,
duration,
level,
startDate,
maxPeople,
category: { name: categoryName, color },
});
});
return output;
};
Example testing in a React Native project
const [events, setEvents] = useState([]);
const [isEventsLoading, setIsEventsLoading] = useState(false);
const getEvents = async () => {
setEvents([]);
setIsEventsLoading(true);
try {
const evts = await getEventsRequest();
setEvents(evts);
setIsEventsLoading(false);
} catch (e) {
console.error(e);
}
};
useEffect(() => {
getEvents();
}, []);
console.log('events', events);
Output
events Array []
Expected
events Array [
{
name : "blabla",
address: "blabla",
city: "blabla",
duration: 60,
level: "hard",
startDate: "13/04/2021",
maxPeople: 7,
category: {
name: "Football",
color: "#fff"
},
},
// ...
]
I don't know if there is a simpler method to retrieve this kind of data (for example there is populate method on mongo DB).
Thank you in advance for your answers.
When you use CollectionReference#get, it returns a Promise containing a QuerySnapshot object. The forEach method on this class is not Promise/async-compatible which is why your code stops working as you expect.
What you can do, is use QuerySnapshot#docs to get an array of the documents in the collection, then create a Promise-returning function that processes each document and then use it with Promise.all to return the array of processed documents.
In it's simplest form, it would look like this:
async function getDocuments() {
const querySnapshot = await firebase.firestore()
.collection("someCollection")
.get();
const promiseArray = querySnapshot.docs
.map(async (doc) => {
/* do some async work */
return doc.data();
});
return Promise.all(promiseArray);
}
Applying it to your code gives:
export const getEventsRequest = async () => {
const querySnapshot = await firebase.firestore()
.collection('events')
.get();
const dataPromiseArray = querySnapshot.docs
.map(async (doc) => {
const {
name,
address,
city,
duration,
level,
startDate,
maxPeople,
categoryId,
} = doc.data();
const { name: categoryName, color } = (
await firebase.firestore().collection('categories').doc(categoryId).get()
).data();
return {
name,
address,
city,
duration,
level,
startDate,
maxPeople,
category: { name: categoryName, color },
};
});
// wait for each promise to complete, returning the output data array
return Promise.all(dataPromiseArray);
};
I'm trying to get all users from api and I need to find the user which most get paid.
so for example
let users=['tom','jenny','smith','Joe']
async function getUsers() {
let response = await fetch(`http://something.something?q=${users}`);
let data = await response.json();
return data;
}
getUsers().then(data =>console.log(data))
so my plan is users[0],users[1] something like a make function which I add index number via loop.
and get all users and find out who get the most paid.
so my question is how can do fetch users step by step.
You can use Array reduce to get the user who less payed and combine with Promise.all using async / await to fetch all user data
'use strict';
/* mock data for testing purposes
const usersData = [{
name: 'john doe',
payed: 10,
}, {
name: 'john doe01',
payed: 5,
}, {
name: 'john doe02',
payed: 8,
}, {
name: 'john doe03',
payed: 20,
}, {
name: 'john doe04',
payed: 40,
}, {
name: 'john doe05',
payed: 37,
}];
*/
async function getUsers() {
const usersResponse = await Promise.all(
usersName.map(userName => fetch(`http://something.something?q=${userName}`))
);
return usersResponse.map(userResponse => userResponse.json());
}
async function init() {
try {
const usersName = [
'tom',
'jenny',
'smith',
'Joe',
];
const usersData = await getUsers(usersName);
const userWhoLessPayed = usersData.reduce((prevUser, currentUser) => {
if (prevUser.payed > currentUser.payed) {
return currentUser;
}
return prevUser;
});
console.log(userWhoLessPayed);
} catch (e) {
console.error(e);
}
}
init();
Maybe try something like this? Do you need to use async?
Basically, the strategy is this:
Return all users. Unless the endpoint has a way to filter (via
parameters - in some way where you currently cannot ascertain the
highest paid artist)
Once returned, take that list of users returned from the endpoint
and filter. Below, we're filtering a returned array of objects,
looking for the highest value of a fictional key called 'pay'.
$.when( getAllArtists() ).then(function (allArtistResults) {
$.each(allArtistResults, function(key, value){
// let's just pretend we get an array of objects back
// you could just filter it here?
// props on below: https://stackoverflow.com/questions/4020796/finding-the-max-value-of-an-attribute-in-an-array-of-objects
Math.max.apply(Math, value.map(function(artists) { return artists.pay; }))
}
}
function getAllArtists() {
return $.ajax({
url: 'your-endpoint-here',
type: 'GET'
});
}