Can't access the index of my array in async-await - javascript

why every time I try to access the index of my array it returns undefined? Also, I've tried to check it using type of it also returns object instead of array.
import { createAsyncThunk } from '#reduxjs/toolkit';
import { firestore } from 'firebase/firebaseConfig';
const RETRIEVE_COMMENTS = createAsyncThunk('retrieveComments', async ({ docID }) => {
try {
let retrievedComments = []
const comments = await firestore
.collection('products')
.doc(docID)
.collection('comments')
.orderBy("timestamp")
comments.onSnapshot(snapshot => {
snapshot.forEach((res) => retrievedComments.push(res.data()))
})
console.log(retrievedComments[0])
} catch (error) {
console.log(error)
}
})
export default RETRIEVE_COMMENTS

snapshot -> constantly listens to the collection
use the Promise returned from your write operation by attaching a .then()
If you want to receive snapshot events when the document or query metadata changes, pass a listen options object when attaching your listener:
db.collection("cities").doc("SF")
.onSnapshot({
// Listen for document metadata changes
includeMetadataChanges: true
}, (doc) => {
// ...
});
in your case :
import { createAsyncThunk } from '#reduxjs/toolkit';
import { firestore } from 'firebase/firebaseConfig';
const RETRIEVE_COMMENTS = createAsyncThunk('retrieveComments', async ({ docID }) => {
try {
let retrievedComments = []
const comments = await firestore
.collection('products')
.doc(docID)
.collection('comments')
.orderBy("timestamp")
comments.onSnapshot(snapshot => {
snapshot.forEach((res) => retrievedComments.push(res.data()))
}).then(doc => console.log(doc))
console.log(retrievedComments[0])
} catch (error) {
console.log(error)
}
})
export default RETRIEVE_COMMENTS
i am sorry if that not what you need I can not comment on your question I do not have more than 50 REPUTATION to comment

Related

Workaround for firebase “IN” limit of 10 compatible with onSnapshot?

I have run into the firebase “IN” limit of 10. Although a workaround solution was already answered here:
Is there a workaround for the Firebase Query "IN" Limit to 10?
None of the solutions in that thread seem to work with the listener “onSnapshot”. For my use case (Vue 3), I have a composable/function call I that queries firebase passing in an array that could have up to 100 document ID values and returns an object as below.
Is this possible?
import { ref, watchEffect } from 'vue'
import { db } from '#/firebase/config'
import { collection, onSnapshot, query, where, documentId } from 'firebase/firestore'
const getUsersList = (idList) => {
// idList is an array of document ID's
const documents = ref(null)
let collectionRef = collection(db, 'users')
collectionRef = query(collectionRef, where(documentId(), 'in', idList))
// this fails if I pass in more than 10 elements in the array
const unsub = onSnapshot(collectionRef, snapshot => {
let results = []
snapshot.docs.forEach(doc => {
results.push({ ...doc.data(), id: doc.id })
})
// update values
documents.value = results
})
watchEffect((onInvalidate) => {
onInvalidate(() => unsub())
})
return { documents }
}
export default getCollectionRt
Since no replies here completely answered the question, I ended up paying a freelancer to take a look and here's what they came up with. The solution does seem to have a random issue I am trying to sort out when the underlying changes, one of the records will disappear. It does work, is in scope of the original question and seems to have solved the problem.
import { ref, watchEffect } from 'vue'
import { db } from '#/firebase/config'
import { collection, onSnapshot, query, where, documentId } from 'firebase/firestore'
const getUserList = (idList) => {
console.log('idList', idList)
let documents = ref(null)
let collectionRef = collection(db, 'users')
let unsub, unsubes = [], resultsList = [{}];
for (let i = 0; i < idList.length; i += 10) {
let idList1 = idList.slice(i, i + 10); //console.log(idList1);
let collectionRef1 = query(collectionRef, where(documentId(), 'in', idList1))
unsub = onSnapshot(collectionRef1, snapshot => {
let results = []
snapshot.docs.forEach(doc => {
results.push({ ...doc.data(), id: doc.id })
})
resultsList.splice(resultsList.length, 0, ...results);
console.log('results', results)
documents.value = results
})
unsubes.push(unsub);
}
watchEffect((onInvalidate) => {
onInvalidate(() =>{ unsubes.forEach(unsub => { unsub(); console.log("unsut", unsub); }) });
})
Promise.all(unsubes);
resultsList.shift(0);
console.log("docu", documents.value);
return { documents };
}
export default getUserList
You will have to initialize multiple listeners i.e. same number of queries but with onSnapshot() (might be better than setting up a listener for each individual document). Try:
import { ref } from 'vue';
import { collection, query, where, documentId, onSnapshot } from 'firebase/firestore'
const users = ref({})
const dataLoaded = ref(false)
const addFirestoreListeners = async () => {
const idList = [];
for (let i = 0; i < idList.length; i += 10) {
const items = idList.slice(i, i + 10)
const q = query(collection(db, 'users'), where(documentId(), 'in', items))
onSnapshot(q, (snapshot) => {
if (dataLoaded.value) {
snapshot.docChanges().forEach((change) => {
if (change.type === 'added' || change.type === 'modified') {
users.value[change.doc.id] = change.doc.data()
} else (change.type === 'removed') {
users.value[change.doc.id] = null
}
})
} else {
snapshot.forEach((doc) => {
users.value[doc.id] = doc.data()
})
dataLoaded.value = true
}
})
}
}
The dataLoaded flag checks if the listeners have fetched data for first time or has received an update. I've use a map where the key is document ID so it can be removed easily but do change that to an array or any other required structure.

Firebase Firestore not deleting a document

I want to delete document in firestore using deleteDoc(), but this not delete.
It's my code:
import { getFirestore } from "firebase/firestore";
import { getStorage, ref } from "firebase/storage";
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
let stRootDocument = `/${tipo}/${uid}`;
const refCol = doc(db,stRootDocument);
deleteDoc(refCol)
.then(() => {
console.log("Entire Document has been deleted successfully.")
})
.catch(error => {
console.log(error);
})
although, this dont delete.
My document don't have any subcollections, but don't delete, also I have tried:
to do the refer using:
const refCol = doc(db,"carousel","xgxxxxxxxxxxxxxxx");
and after to pass this refer in the deleteDoc Method but too don't delete.
other form:
deleteDoc(doc(colRef,uid)).then((objR) => {
return objR;
}).catch((error) => {
console.log(error);
});
but, this too doesn't delete.

Array.map() doesn't render anything in React

I'm trying to make a list in my react app. I have retrieved data from my database, and pushed it into a list. I have doublechecked that the data shows up correctly in the console, and it does, but array.map() returns nothing. I think the problem might be that array.map() runs two times. I don't know why it runs two times.
function Dashboard() {
const user = firebase.auth().currentUser;
const [teams, setTeams] = useState([])
const history = useHistory();
useEffect(() => {
getTeams()
if (user) {
} else {
history.push("/")
}
}, [])
function Welcome() {
if (user) {
return <h1>Welcome, {user.displayName}</h1>
} else {
}
}
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = []
docList.forEach((data) => {
firebase.firestore().collection('teams').doc(data.teamId).get().then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
})
setTeams(teamslist)
})
}
const openTeam = (data) => {
console.log(data.teamId)
}
return (
<div>
<Welcome />
<div>
<ul>
{console.log(teams)}
{teams.map((data) => {
return (
<li onClick={() => openTeam(data)} key={data.teamId}>
<h1>{data.name}</h1>
<p>{data.teamId}</p>
</li>
)
})}
</ul>
</div>
</div>
)
}
export default Dashboard
The getTeams function has a bug where it isn't waiting for the firebase.firestore().collection('teams').doc(data.teamId).get().then promises to finish before calling setTeams, so it is called with an empty array, causing React to trigger a render with the empty array.
As the promises for fetching each team resolve they will be pushed to the same array reference, but this won't trigger a rerender in React since you're not calling setTeams again when the array changes.
Try this code, which won't call setTeams until each team promise generated from docList has been resolved.
const getTeams = () => {
firebase.firestore().collectionGroup('members').where('user', '==', user.uid).get().then((snapshot) => {
const docList = []
snapshot.forEach((doc) => {
docList.push({
teamId: doc.data().teamId,
})
})
const teamslist = [];
Promise.all(docList.map((data) => {
return firebase
.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
})
})
}))
.then(() => setTeams(teamslist));
})
}
A smaller edit would be to call setTeams after each separate team promise resolves, which will trigger a React render each time a new team is resolved:
.then((doc) => {
teamslist.push({
name: doc.data().name,
teamId: doc.id,
});
// create a new array, since using the same array
// reference won't cause react to rerender
setTeams([...teamslist]);
})
Many thanks to #martinstark who provided you an answer while I was unavailable.
However, there are some more things that need to be covered.
User State
In your current component, you pull the current user from Firebase Authentication, but don't handle the state changes of that user - signing in, signing out, switching user. If a user is signed in and they were to navigate directly to your dashboard, firebase.auth().currentUser could be momentarily null while it resolves the user's login state, which would incorrectly send them off to your login page.
This can be added using:
const [user, setUser] = useState(() => firebase.auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => firebase.auth().onAuthStateChanged(setUser), []);
Next, in your first useEffect call, you call getTeams() whether the user is signed in or not - but it should depend on the current user.
useEffect(() => {
if (userLoading) {
return; // do nothing (yet)
} else if (user === null) {
history.push("/");
return;
}
getTeams()
.catch(setError);
}, [user]);
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const docList = []
membersQuerySnapshot.forEach((doc) => {
docList.push({
teamId: doc.get("teamId"), // better perfomance than `doc.data().teamId`
});
});
const teamDataList = [];
await Promise.all(docList.map((data) => {
return firebase.firestore()
.collection('teams')
.doc(data.teamId)
.get()
.then(doc => teamDataList.push({
name: doc.get("name"),
teamId: doc.id
}));
}));
setTeams(teamDataList);
}
Optimizing getTeams() - Network Calls
The getTeams function in your question calls setTeams with the array [], which will be empty at the time of calling it as covered in #martinstark's answer. The "get team data" operations are asyncronous and you aren't waiting for them to resolve before updating your state and triggering a new render. While you are pushing data to them after the component has rendered, modifying the array won't trigger a new render.
While you could fetch the data for each team using db.collection("teams").doc(teamId).get(), each of these is requests is a network call, and you can only make a limited number of these in parallel. So instead of fetching 1 team per network call, you could fetch up to 10 teams per network call instead using the in operator and FieldPath.documentId().
Assuming the collectionGroup("members") targets the collections of documents at /teams/{aTeamId}/members which contain (at least):
"/teams/{aTeamId}/members/{memberUserId}": {
teamId: aTeamId,
user: memberUserId, // if storing an ID here, call it "uid" or "userId" instead
/* ... */
}
// this utility function lives outside of your component near the top/bottom of the file
function chunkArr(arr, n) {
if (n <= 0) throw new Error("n must be greater than 0");
return Array
.from({length: Math.ceil(arr.length/n)})
.map((_, i) => arr.slice(n*i, n*(i+1)))
}
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const chunkedTeamIDList = chunkArr(teamIDList, 10) // split into batches of 10
const teamsColRef = firebase.firestore().collection('teams');
const documentId = firebase.firestore.FieldPath.documentId(); // used with where() to target the document's ID
const foundTeamDocList = await Promise
.all(chunkedTeamIDList.map((chunkOfTeamIDs) => {
// fetch each batch of IDs
return teamsColRef
.where(documentId, 'in', chunkOfTeamIDs)
.get();
}))
.then((arrayOfQuerySnapshots) => {
// flatten results into a single array
const allDocsList = [];
arrayOfQuerySnapshots.forEach(qs => allDocsList.push(...qs.docs));
return allDocsList;
});
const teamDataList = foundTeamDocList
.map((doc) => ({ name: doc.get("name"), teamId: doc.id }));
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
You can also make use of this utility function to simplify & optimize the code a bit. Which gives:
// This getTeams() is a () => Promise<void>
const getTeams = async () => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', user.uid)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// update state & trigger render
setTeams(teamDataList);
}
Optimizing getTeams() - Function Definition
As part of the last optimization, you could pull it out of your component or place it in its own file so that it's not redefined with every render:
// define at top/bottom of the file outside your component
// This getTeams() is a (userId: string) => Promise<{ name: string, teamId: string}[]>
async function getTeams(userId) => {
const membersQuerySnapshot = await firebase.firestore()
.collectionGroup('members')
.where('user', '==', userId)
.get();
const teamIDList = []
membersQuerySnapshot.forEach((doc) => {
teamIDList.push(doc.get("teamId")); // better perfomance than `doc.data().teamId`
})
const teamsColRef = firebase.firestore().collection('teams');
const teamDataList = [];
await fetchDocumentsWithId(
teamsColRef,
teamIDList,
(doc) => teamDataList.push({ name: doc.get("name"), teamId: doc.id })
);
// sort by name, then by ID
teamDataList.sort((aTeam, bTeam) =>
aTeam.name.localeCompare(bTeam.name) || aTeam.teamId.localeCompare(bTeam.teamId)
)
// return the sorted teams
return teamDataList
}
and update how you use it:
useEffect(() => {
if (userLoading) {
return; // do nothing
} else if (user === null) {
history.push("/");
return;
}
getTeams(user.uid)
.then(setTeams)
.catch(setError);
}, [user]);

How to pre-fetch data using prefetchQuery with React-Query

I am trying to pre-fetch data using react-query prefetchQuery. When I am inspecting browser DevTools network tab I can see that data that was requested for prefetchQuery is coming from the back-end but for some reason when I look into react-query DevTools it does generate the key in the cache but for some reason the Data is not there. Let me know what I am doing wrong.
import { useState, useEffect } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import axios from 'axios';
const baseURL = process.env.api;
async function getSubCategoryListByCategoryId(id) {
// await new Promise((resolve) => setTimeout(resolve, 300));
console.log(`${baseURL}/category/subcategories/${id}`);
try {
const { data } = await axios.request({
baseURL,
url: `/category/subcategories/${id}`,
method: 'get',
});
console.log('data getSubCategoryListByCategoryId index: ', data);
return data;
} catch (error) {
console.log('getSubCategoryListByCategoryId error:', error);
}
}
// const initialState = {
// };
const ProductCreate = () => {
const [values, setValues] = useState(initialState);
const queryClient = useQueryClient();
const { data, isLoading, isError, error, isFetching } = useQuery(
'categoryList',
getPosts
);
const dataList = JSON.parse(data);
useEffect(() => {
setValues({ ...values, categories: dataList });
dataList.map((item) => {
console.log('useEffect values.categories item.id: ', item._id);
queryClient.prefetchQuery(
['subCategoryListByCategoryId', item._id],
getSubCategoryListByCategoryId(item._id)
);
});
}, []);
return <h1>Hello</h1>;
};
export default ProductCreate;
The second parameter to prefetchQuery expects a function that will fetch the data, similar to the queryFn passed to useQuery.
But here, you are invoking the function, thus passing the result of it into prefetchQuery:
getSubCategoryListByCategoryId(item._id)
if you want to do that, you can manually prime the query via queryClient.setQueryData, which accepts a key and the data for that key passed to it.
otherwise, the fix is probably just:
() => getSubCategoryListByCategoryId(item._id)

What is the correct way to do an async action using redux observable and firebase?

so I have this epic
export const listenToCountryVisitsEpic = (action$, store) => {
return action$.pipe(
ofType('UPDATE_TRIP_DETAILS'),
mergeMap((action) => {
const userId = store.value.user.id
const { country, newDetails } = action
const { people, places } = details
const scheme = {
people,
places,
}
firebase.database().ref(`users/${userId}/path`).update(scheme)
return mergeMap((response) => {
const payload = { ...JSON.parse(response.request.body), currentDetails: action.currentDetails }
return [updatedCountrySuccess(payload)]
})
}),
)
}
what I want to do is make the request to firebase and then if successful update redux which in turn will update react. if not successful then catch it
this is correctly posting to firebase but I need to catch errors correctly. I don't want to fire this: updatedCountrySuccess until I know it's a success or failure in firebase
can I use async/await for this?
You can use from() to turn Promise into Observable and then catchError to replace the error with whatever you want.
import { from } from 'rxjs';
import { catchError } from 'rxjs/operators';
...
return action$.pipe(
ofType('UPDATE_TRIP_DETAILS'),
mergeMap((action) => {
const userId = store.value.user.id
const { country, newDetails } = action
const { people, places } = details
const scheme = {
people,
places,
}
return from(firebase.database().ref(`users/${userId}/path`).update(scheme))
.pipe(
// `mergeMap` will handle only `next` notifications
mergeMap((response) => {
const payload = ...
return [updatedCountrySuccess(payload)]
}),
catchError(error => of(errorAction)),
);
}),
)

Categories

Resources