How to get all documents in Firestore (v9) sub collection - javascript

I'm trying to get all documents in all sub-collection with Firebase V9, so I used collectionGroup to retrieve the data.
Here is my Firestore architecture :
bots (collection)
| id (doc ID)
| order_history (sub collection)
| id (doc ID)
createdBy: uid (user uid)
And this is how I try to get documents :
const [orders, setOrders] = useState([]);
const { user } = useAuth();
const getOrders = async () => {
const orders = await getDocs(query(collectionGroup(db, 'order_history'), where('createdBy', '==', user.uid)));
setOrders(orders.docs.map((doc) => ({
...doc.data()
})))
}
useEffect(() => {
getOrders();
console.log('orders ', orders); // return []
}, []);
This code returns an empty array.
Did I do something wrong?

I think your getOrders function is asynchronus function.
If you want debug log I think you should waiting for getOrders completed then orders had been updated.
Ex:
useEffect(() => {
console.log('orders ', orders);
}, [orders]);

Your getOrders anonymous method execution requires an explicit return statement if there's more than 1 statement. Implicit returns work when only a single statement exists (and after some testing, return await X doesn't appear to work either).
const getOrders = async () => {
const orders = await getDocs(...);
setOrders(orders.docs.map(...))
}
Needs to be
const getOrders = async () => {
const orders = await getDocs(...);
return setOrders(orders.docs.map(...))
}

Related

My async await method for fetching docs in Firestore using getDocs() returns an empty array

My async await method for fetching docs in Firestore using getDocs() returns an empty array.
I'm using React.js.
Thing is, this fetching-data-function is placed within a useEffect() hook with an empty array [] as a dependency (so that it runs, and hence fetches data from the Firestore database only once), and right after, the data is console logged. Output is an empty array.
allData: []
But if I just somehow get the useEffect() hook to run once more (like making a tiny change in the code and saving it - essentially just refreshing it on the local host), the array is populated with the desired data from the database.
This is the code:
import db from "./firebase";
useEffect(() => {
console.log("use effect ran");
const temp = async () => {
const q = query(collection(db, "blogs"));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
const newblog = {
id: doc.id,
title: doc.data().title,
content: doc.data().content,
};
setAllData((prev) => [...prev, newblog]);
});
};
temp();
console.log("allData: ", allData);
}, []);
I can't quite figure out what the issue is. I'd be grateful for some help.
You are seeing a empty array in allData because the console.log("allData: ", allData) is running before than setAllData((prev) => [...prev, newblog]) because the async function temp doesn't run in the same thread so the interpreter continues reading the code that follows. To fix it add await to temp() like this: await temp() and wrap it like I do in the following code.
import db from "./firebase";
useEffect(() => {
console.log("use effect ran");
(async () => {
const temp = async () => {
const q = query(collection(db, "blogs"));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
const newblog = {
id: doc.id,
title: doc.data().title,
content: doc.data().content
};
setAllData((prev) => [...prev, newblog]);
});
};
await temp();
console.log("allData: ", allData);
})();
}, []);

How to get specific data from all documents from a collection in firebase?

Platform: React Native (Expo)
So I'm trying to get two values (dotCoins and name) from my firebase db and I can't figure out how to go about it. Here's my firebase structure:
This is what I currently have in place:
// Calling function when screen loads
componentDidMount() {
this.getDotCoins();
this.getUserData();
}
// Calling function when it updates
componentDidUpdate() {
this.getDotCoins();
this.getUserData();
}
// The function
getUserData = async () => {
const querySnapshot = await getDocs(collection(db, "users"));
const tempDoc = [];
querySnapshot.forEach((doc) => {
console.log(doc.id, " => ", doc.data());
});
console.log(tempDoc);
};
Both the console.log() prints nothing, and my console remains absolutely empty. I can't find where I'm going wrong since I don't receive any errors too. (I have all packages installed correctly and all functions imported too)
You are not pushing any document data to tempDoc so it'll always be empty. Try refactoring the code as shown below:
getUserData = async () => {
const querySnapshot = await getDocs(collection(db, "users"));
const tempDoc = querySnapshot.docs.map((d) => ({
id: d.id,
...d.data()
}));
console.log(tempDoc);
};
const q = query(collection(db, "users"));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.data())
})
});
});
return unsubscribe;

Firebase Realtime database query from v8 to v9 SDK

I have a Firebase Realtime database query written using Firebase version 8 that I'm looking to migrate over to the v9 SDK.
export const getSingleDataWithQuery = async ({ key, email, criteria }) => {
if (!criteria) return;
const snapshot = await realTimeDb
.ref()
.child(key)
.orderByChild(query)
.equalTo(criteria)
.get();
const val = snapshot.val();
if (val) {
const keys = Object.keys(val);
return val[keys[0]];
}
return null;
};
In this example:
key would be the 'users' collection
the email field is looking for users by their email
and the criteria is the user's actual email (jane.doe#gmail.com)
Using Firebase's Read data once and Sorting data documentation I managed to narrow it down to perhaps being this, but I'm not sure if it's correct:
export const getSingleDataWithQuery = async ({ key, query, criteria }) => {
if (!criteria) return;
const dbRef = query(ref(realTimeDb, key), orderByChild(email), equalTo(criteria));
get(dbRef).then(snapshot => {
if (snapshot.exists()) {
const val = snapshot.val();
if (val) {
const keys = Object.keys(val);
return val[keys[0]];
}
}
});
return null;
};
Aside from the fact that you may have swapped query and email in the fragments, the only difference is in the way you handle the asynchronous database call and that likely explains the difference. Since you use then in the second snippet, the function doesn't actually return a promise and so calling it with await it in the caller won't have any effect.
To make those the same again, use await in the second snippet too, instead of then:
export const getSingleDataWithQuery = async ({ key, query, criteria }) => {
if (!criteria) return;
const dbRef = query(ref(realTimeDb, key), orderByChild(email), equalTo(criteria));
const snapshot = await get(dbRef); // 👈
if (snapshot.exists()) {
const val = snapshot.val();
if (val) {
const keys = Object.keys(val);
return val[keys[0]];
}
}
return null;
};

setState in nested async function - React Hooks

How can I build a function which gets some data asynchronously then uses that data to get more asynchronous data?
I am using Dexie.js (indexedDB wrapper) to store data about a direct message. One thing I store in the object is the user id which I'm going to be sending messages to. To build a better UI I'm also getting some information about that user such as the profile picture, username, and display name which is stored on a remote rdbms. To build a complete link component in need data from both databases (local indexedDB and remote rdbms).
My solution returns an empty array. It is being computed when logging it in Google Chrome and I do see my data. However because this is not being computed at render time the array is always empty and therefor I can't iterate over it to build a component.
const [conversations, setConversations] = useState<IConversation[]>()
const [receivers, setReceivers] = useState<Profile[]>()
useEffect(() => {
messagesDatabase.conversations.toArray().then(result => {
setConversations(result)
})
}, [])
useEffect(() => {
if (conversations) {
const getReceivers = async () => {
let receivers: Profile[] = []
await conversations.forEach(async (element) => {
const receiver = await getProfileById(element.conversationWith, token)
// the above await is a javascript fetch call to my backend that returns json about the user values I mentioned
receivers.push(receiver)
})
return receivers
}
getReceivers().then(receivers => {
setReceivers(receivers)
})
}
}, [conversations])
/*
The below log logs an array with a length of 0; receivers.length -> 0
but when clicking the log in Chrome I see:
[
0: {
avatarURL: "https://lh3.googleusercontent.com/..."
displayName: "Cool guy"
userId: "1234"
username: "cool_guy"
}
1: ...
]
*/
console.log(receivers)
My plan is to then iterate over this array using map
{
receivers && conversations
? receivers.map((element, index) => {
return <ChatLink
path={conversations[index].path}
lastMessage={conversations[index].last_message}
displayName={element.displayName}
username={element.username}
avatarURL={element.avatarURL}
key={index}
/>
})
: null
}
How can I write this to not return a empty array?
Here's a SO question related to what I'm experiencing here
I believe your issue is related to you second useEffect hook when you attempt to do the following:
const getReceivers = async () => {
let receivers: Profile[] = []
await conversations.forEach(async (element) => {
const receiver = await getProfileById(element.conversationWith, token)
receivers.push(receiver)
})
return receivers
}
getReceivers().then(receivers => {
setReceivers(receivers)
})
}
Unfortunately, this won't work because async/await doesn't work with forEach. You either need to use for...of or Promise.all() to properly iterate through all conversations, call your API, and then set the state once it's all done.
Here's is a solution using Promise.all():
function App() {
const [conversations, setConversations] = useState<IConversation[]>([]);
const [receivers, setReceivers] = useState<Profile[]>([]);
useEffect(() => {
messagesDatabase.conversations.toArray().then(result => {
setConversations(result);
});
}, []);
useEffect(() => {
if (conversations.length === 0) {
return;
}
async function getReceivers() {
const receivers: Profile[] = await Promise.all(
conversations.map(conversation =>
getProfileById(element.conversationWith, token)
)
);
setReceivers(receivers);
}
getReceivers()
}, [conversations]);
// NOTE: You don't have to do the `receivers && conversations`
// check, and since both are arrays, you should check whether
// `receivers.length !== 0` and `conversations.length !== 0`
// if you want to render something conditionally, but since your
// initial `receivers` state is an empty array, you could just
// render that instead and you won't be seeing anything until
// that array is populated with some data after all fetching is
// done, however, for a better UX, you should probably indicate
// that things are loading and show something rather than returning
// an empty array or null
return receivers.map((receiver, idx) => <ChatLink />)
// or, alternatively
return receivers.length !== 0 ? (
receivers.map((receiver, idx) => <ChatLink />)
) : (
<p>Loading...</p>
);
}
Alternatively, using for...of, you could do the following:
function App() {
const [conversations, setConversations] = useState<IConversation[]>([]);
const [receivers, setReceivers] = useState<Profile[]>([]);
useEffect(() => {
messagesDatabase.conversations.toArray().then(result => {
setConversations(result);
});
}, []);
useEffect(() => {
if (conversations.length === 0) {
return;
}
async function getReceivers() {
let receivers: Profile[] = [];
const profiles = conversations.map(conversation =>
getProfileById(conversation.conversationWith, token)
);
for (const profile of profiles) {
const receiver = await profile;
receivers.push(receiver);
}
return receivers;
}
getReceivers().then(receivers => {
setReceivers(receivers);
});
}, [conversations]);
return receivers.map((receiver, idx) => <ChatLink />);
}
i think it is happening because for getReceivers() function is asynchronous. it waits for the response, in that meantime your state renders with empty array.
you can display spinner untill the response received.
like
const[isLoading,setLoading]= useState(true)
useEffect(()=>{
getReceivers().then(()=>{setLoading(false)}).catch(..)
} )
return {isLoading ? <spinner/> : <yourdata/>}
Please set receivers initial value as array
const [receivers, setReceivers] = useState<Profile[]>([])
Also foreach will not wait as you expect use for loop instead of foreach
I am not sure it is solution for your question
but it could help you to solve your error

How to make a Firebase messaging function asynchronous

I have a firebase messaging function, but the return function seems to execute before the rest of the functions. Here is the code (sorry its long):
exports.newMessageNotification = functions.firestore
.document(`messages/{messageId}`) // wildcard for the msg id
.onCreate(async (change, context) => {
const db = admin.firestore();
const messageId: string = context.params.messageId;
const messageRef = db.collection('messages').doc(messageId);
const tokens = [];
// get the message
const message: Message = await messageRef
.get()
.then(q => q.data() as Message);
const recipients: any = await message.recipients;
const user: User = await db
.collection('users')
.doc(message.senderId)
.get()
.then(q => {
return q.data() as User;
});
// Notification content
const payload = await {
notification: {
title: `${user.name}`,
body: `${message.message}`,
},
};
console.log(payload);
// loop though each recipient, get their devices and push to tokens
Object.keys(recipients).forEach(async recipient => {
const devicesRef = db
.collection('devices')
.where('userId', '==', recipient);
const devices = await devicesRef.get();
devices.forEach(res => {
const token: string = res.data().token;
console.log(token);
tokens.push(token);
});
});
console.log(tokens); // logs empty
return await sendToCloud(tokens, payload);
});
I think the issue is that the forEach is not asynchronous so the final line of code is executing before waiting for forEach to finish?
Ugh. I had this problem somewhere recently. You are correct, at least in my experience: forEach does not seem to obey the async directive. I had to use the for ... in ... syntax to:
Get it to obey async (from the parent scope)
Process sequentially, as order was important to me at the time
In your case, it would probably look like for (const recipient in recipients) { ... }
Apparently, this is caused by forEach calling the callback on each item without awaiting a response. Even if the callback is asynchronous, forEach doesn't know to await its response on each loop.
Source: https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404
A solution from the comments on the blog linked above:
await Promise.all(
Object.keys(recipients).map(async recipient => {
const devicesRef = db
.collection('devices')
.where('userId', '==', recipient);
const devices = await devicesRef.get();
devices.forEach(res => {
const token: string = res.data().token;
console.log(token);
tokens.push(token);
});
});
)
Note the forEach has been replaced by a map, and it's within Promise.all(...).

Categories