Use firebase onSnapshot() in for loop? - javascript

The below code works for getting my data from firestore. I'm trying to update this to use onSnapshot() instead of of get(). Maybe the core of my confusion is onSnapshot() doesn't return a promise and I've tried just adding the listeners into an array but it seems the data doesn't get updated. How do I iterate over a for loop of onSnapshot()'s and render the results?
const [activityDataArray, setActivityDataArray] = useState([]);
const userActivityIds = userData.activities
useEffect(() => {
let promises = [];
for (const activityId of userActivityIds) {
promises.push(getFirestoreData("activities", activityId));
}
Promise.all(promises).then(response => setActivityDataArray(response));
}, [userActivityIds]);
UPDATED CODE:
When I console.log() the array it has my data, but I think this is a trick with chrome dev tools showing new information. I think when I call setActivityDataArray it's running it on an empty array and then it never gets called again. So the data doesn't render unless I switch to a different tab in my application and go back. Then it renders correctly (so I know the data is good, it's just a rendering issue). I think I need to re-render within onSnapshot() but how do I do this correctly?
const [activityDataArray, setActivityDataArray] = useState<any>([]);
const userActivityIds: string[] = userData.activities
useEffect(() => {
let activityDataArrayDummy: any[] = []
for (const i in userActivityIds) {
firebase.firestore().collection("activities").doc(userActivityIds[i])
.onSnapshot((doc) => {
activityDataArrayDummy[i] = doc.data();
});
}
console.log("activityDataArrayDummy", activityDataArrayDummy)
setActivityDataArray(activityDataArrayDummy);
}, [userActivityIds]);

Simply calling onSnapshot() in a loop should do it.
import { doc, onSnapshot } from "firebase/firestore";
for (const activityId of userActivityIds) {
// get reference to document
const docRef = doc(db, "activities", activityId)
onSnapshot(docRef, (snapshot) => {
// read and render data from snapshot
})
}
However, if you ever need to unsubscribe from any of the listeners then you might have to store the Unsubscribe function returned by onSnapshot
somewhere in state.
Just in case you have 10 or less items in userActivityIds then you can use onSnapshot() with a Query instead:
const q = query(collection(db, "activities"), where(documentId(), "in", userActivityIds));
onSnapshot(q, (querySnapshot) => {
// ...
})

I went with this. Apparently state in react isn't guaranteed to be updated, so the proper way to setState with updated data is using a callback function.
https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
const [activityDataObj, setActivityDataObj] = useState({})
const userActivityIds: string[] = userData.activities
useEffect(() => {
for (const i in userActivityIds) {
firebase.firestore().collection("activities").doc(userActivityIds[i])
.onSnapshot((doc) => {
setActivityDataObj((activityDataObj) => {
return {...activityDataObj, [userActivityIds[i]]: doc.data()}
})
});
}
}, [userActivityIds]);

Related

I have an array of different IDs and I want to fetch data from each IDs . Is it possible?

I have an array of mongoDB ids.
const pId =  ['62b3968ad7cc2315f39450f3', '62b37f9b99b66e7287de2d44']
I used forEach to seperate the IDs like :
pId.forEach((item)=>{
console.log(item)
})
but I have a database(products) from where I want to fetch data from. So I tried
const [product, setProduct] = useState([{}]);
useEffect(() => {
pId?.forEach((item) => {
const getProduct = async () => {
try {
const res = await userRequest.get("/products/find/" + item)
setProduct(res.data)
} catch (err) {
console.log(err)
}
}
getProduct()
})
}, [pId])
I used useState[{}] because I want to collect the data in an array of objects.
I used useState[{}] because I want to collect the data in an array of objects.
Your code isn't collecting objects into an array. It's setting each result of the query as the single state item (overwriting previous ones).
If you want to get all of them as an array, build an array; one way to do that is map with the map callback providing a promise of each element, then use Promise.all to wait for all of those promises to settle:
// The usual advice is to use plurals for arrays, not singulars ("products", not "product")
const [products, setProducts] = useState([]); // Start with an empty array
useEffect(() => {
if (pId) {
Promise.all(pId.map((item) => userRequest.get("/products/find/" + item)))
.then((products) => setProducts(products))
.catch((error) => console.log(error));
}
}, [pId]);
Note that if pId changes while one or more userRequest.get calls are still outstanding, you'll get into a race condition. If userRequest.get provides a way to cancel in-flight calls (like fetch does), you'll want to use that to cancel the in-flight calls using a cleanup callback in the useEffect. For example, if userRequest.get accepted an AbortSignal instance (like the built-in fetch does), it would look like this:
const [products, setProducts] = useState([]);
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
if (pId) {
Promise.all(pId.map((item) => userRequest.get("/products/find/" + item, { signal })))
.then((products) => setProducts(products))
.catch((error) => {
if (!signal.aborted) {
console.log(error);
}
});
}
return () => {
controller.abort();
};
}, [pId]);
Again, that's conceptual; userRequest.get may not accept an AbortSignal, or may accept it differently; the goal there is to show how to cancel a previous request using a useEffect cleanup callback.
You can map through the ids and create a promise for each, then use Promise.all() and at last set the products state:
import React, {
useState,
useEffect
} from 'react'
const Example = () => {
const [products, setProducts] = useState([]);
const ids = ['62b3968ad7cc2315f39450f3', '62b37f9b99b66e7287de2d44']
useEffect(() => {
if(ids) Promise.all(ids.map(id => userRequest.get("/products/find/" + id).then(r => r.data))).then(results => setProducts(results))
}, [ids])
}
I renamed some of the variables for clarity to the future visitors. (pId to ids and product to products).
You're overwriting your product array with an individual result from each request. A simple solution would be to append to the array instead:
setProduct(product => [...product, res.data]); // take the old array and append the new item
As T.J. Crowder rightly suggested in the comments, you would keep appending to the initial product state when using the simple setter, so you need to use callback form which receives the current state as a parameter and returns the update.
I suggest you rename that particular state to products/setProducts to make clear it's an array.
Not directly related to the question, but bear in mind that firing a huge number of individual requests may cause performance degradation on the client and backend; there are plenty of options to deal with that, so I am not going into more detail here.
yes, it's possible. just change your code a bit:
const [product, setProduct] = useState([]); // empty array is enough for initialzing
useEffect(() => {
async function doSomethingAsync() {
if(!pId) return;
let newArray = [];
for(let item of pId) {
try {
const res = await userRequest.get("/products/find/" + item)
newArray.push(res.data); // push data to temporary array
} catch (err) {
console.log(err)
}
}
// set new state once
setProduct(newArray);
}
doSomethingAsync();
}, [pId])

forEach loop returning the value but my react useState value only returning one array obj

const user = useSelector((state) => state.user);
const [allPost, setAllPost] = useState([]);
const getAllPost = async () => {
const q = query(collection(db, "posts", "post", user.user.email));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
console.log(doc.data());
const newPost = doc.data();
setAllPost([...allPost, newPost]);
});
};
useEffect(() => {
getAllPost();
}, []);
I have four documents in my firestore but setState only returning one although using forEach loop i can see all four object
The problem is that each time you use
setAllPost([...allPost, newPost]);
allPost is still the previous value because state value updates are asynchronous; allPost won't contain the new value until the next render.
You can either use the functional version of the state setter...
querySnapshot.forEach(doc => {
setAllPost(prev => prev.concat(doc.data()));
});
or simply build up a separate array and set them all at once
setAllPost(querySnapshot.docs().map(doc => doc.data()));
Personally, I'd prefer the latter.

Iterate through list of document uids and create listeners in Firestore

In my Vuex store, I have an action that takes a list of uids of followed users from the current user's Firestore UserDataCollection document, iterates through them, and produces the data for each document to be shown on the UI. It works fine using .get(), but I'm trying to convert it to .onSnapshot() so that I can get real-time updates.
I have been totally unsuccessful trying to use .onSnapshot(), as I can't find any references online or in the Firebase docs on how to implement this after mapping through the array of uids.
I tried removing the promises, since onSnapshot doesn't seem to work with promises, and replaced the .get() with .onSnapshot(), but that didn't work.
Does anyone know the correct way to implement the Firestore .onSnapshot() listener given the code below?
getCircle({state, commit}) {
const circle = state.userProfile.circle
let promises = circle.map(u => userDataCollection.doc(u).get())
return Promise.all(promises)
.then(querySnapShot => {
let circleData = []
if (querySnapShot.empty) {
console.log("empty")
} else {
querySnapShot.forEach(doc => {
let item = doc.data()
circleData.push(item)
}
)
}
commit('setUserCircle', circleData)
})
},
Edit based on response
I added .onSnapshot within the forEach as shown in the code below. In vue devtools it's showing the correct number of data entries in my Vuex store, however they are all undefined.
getCircle({state, commit}) {
const circle = state.userProfile.circle
let promises = circle.map(u => userDataCollection.doc(u).get())
return Promise.all(promises)
.then(querySnapShot => {
let circleData = []
if (querySnapShot.empty) {
console.log("empty")
} else {
querySnapShot.forEach(x => {
let itemId = x.data().uid
userDataCollection.doc(itemId)
.onSnapshot((doc) => {
let item = doc.data()
console.log(doc.data())
circleData.push(item)
})
}
)
}
commit('setUserCircle', circleData)
})
},
In presented code commit is run only once before onSnapshot callback. I tried to create example showing the mechanism (I am assuming that circle array is assigned properly and I have 3 items hardcode'ed in mine):
let promises = circle.map(u => userDataCollection.doc(u).get())
Promise.all(promises)
.then( querySnapShot => {
var circleData = []
querySnapShot.forEach( x => {
let itemId = x.data().uid
userDataCollection.doc(itemId).onSnapshot(doc => {
let item = doc.data()
circleData.push(item)
console.log("inside snapshot: ",circleData.length)
})
})
console.log("outside foreach: ",circleData.length)
})
If you run this code you should see in console something like this (I have run this in node):
outside foreach: 0
inside snapshot: 1
inside snapshot: 2
inside snapshot: 3
And if you do any change in Firestore you will get another console log inside snapshot: 4.
I do not have full understanding of your application logic, however I think that commit statement should be inside onSnapshot listener. But of course, with information I have, this is only guess.
Thank you everyone for the help. The solution that worked was to remove the .map and Promise and used an 'in' query. Code shown below:
getCircle({state, commit}) {
const circle = state.userProfile.circle
userDataCollection.where('__name__', 'in', circle).onSnapshot(querySnapShot => {
var circleData = []
querySnapShot.forEach(doc => {
let item = doc.data()
circleData.push(item)
commit('setUserCircle', circleData)
})
})
},

async/await function not always behave correctly

I'm developing a react-native/nodeJS project and I'm experiencing issues with the Axios API call to my backend using async/await functions.
Here's the code:
const TimeTable = () => {
const [attendedCourses, setAttendedCourses] = useState([]);
const [courseSchedules, setCourseSchedules] = useState([]);
useEffect(() => {
getUserCourses();
getCourseSchedule();
console.log(courseSchedules);
}, []);
const getCourseSchedule = async () => {
for (const item of attendedCourses) {
try {
const res = await axios.get(`/api/lesson/findById/${item.courseId}`);
setCourseSchedules((prev) => [
...prev,
{
id: res.data._id,
name: res.data.name,
schedule: [...res.data.schedule],
},
]);
} catch (err) {
const error = err.response.data.msg;
console.log(error);
}
}
};
const getUserCourses = async () => {
const userId = "12345678"; //hardcoded for testing purpose
try {
const res = await axios.get(`/api/users/lessons/${userId}`);
setAttendedCourses(res.data);
} catch (err) {
const error = err.response.data.msg;
console.log(error);
}
};
return (...); //not important
};
export default TimeTable;
The method getUserCourses() behave correctly and returns always an array of objects which is saved in the attendedCourses state. The second method getCourseSchedule() doesn't work correctly. The console.log() in the useEffect() prints most of the time an empty array.
Can someone please explain to me why? Thank you!
While the method is async, the actual useEffect is not dealing it in asynchronous manner and won't await till you reach the console.log in the useEffect. If you put the console.log inside the getCourseSchedule method and log the result after the await, it'll show you correct result every time.
You are confusing the async nature of each method. Your code does not await in the useEffect, it awaits in the actual method while the rest of the useEffect keeps executing.
If you really want to see the result in useEffect, try doing:
useEffect(() => {
const apiCalls = async () => {
await getUserCourses();
await getCourseSchedule();
console.log(courseSchedules);
}
apiCalls()
})
Your useEffect has an empty array as dependencies that means it is run only onetime in before initial render when the courseSchedules has initial value (empty array)
To see the courseSchedules when it change you should add another useEffect like this:
useEffect(() => {
console.log(courseSchedules);
}, [courseSchedules]);

How to get fetch api results in execution order with async/await?

After an input change in my input element, I run an empty string check(if (debouncedSearchInput === "")) to determine whether I fetch one api or the other.
The main problem is the correct promise returned faster than the other one, resulting incorrect data on render.
//In my react useEffect hook
useEffect(() => {
//when input empty case
if (debouncedSearchInput === "") autoFetch();
//search
else searchvalueFetch();
}, [debouncedSearchInput]);
searchvalueFetch() returned slower than autoFetch() when I emptied the input. I get the delayed searchvalueFetch() data instead of the correct autoFetch() data.
What are the ways to tackle this? How do I queue returns from a promises?
I read Reactjs and redux - How to prevent excessive api calls from a live-search component? but
1) The promise parts are confusing for me
2) I think I don't have to use a class
3) I would like to learn more async/await
Edit: added searchvalueFetch, autoFetch, fetcharticles code
const autoFetch = () => {
const url = A_URL
fetchArticles(url);
};
const searchNYT = () => {
const url = A_DIFFERENT_URL_ACCORDING_TO_INPUT
fetchArticles(url);
};
const fetchArticles = async url => {
try{
const response = await fetch(url);
const data = await response.json();
//set my state
}catch(e){...}
}
This is an idea how it could looks like. You can use promises to reach this. First autoFetch will be called and then searchvalueFetch:
useEffect(() => {
const fetchData = async () => {
await autoFetch();
await searchvalueFetch();
};
fetchData();
}, []);
You can also use a function in any lifecycle depends on your project.
lifecycle(){
const fetchData = async () => {
try{
await autoFetch();
await searchvalueFetch();
} catch(e){
console.log(e)
}
};
fetchData();
}
}

Categories

Resources