INTRODUCTION
I have a list of items with the prop "date" stored on my FireStore. In the client code, I have a FlatList with all of those items ordered by "date" (the first element is the most recent item, the second one, the item I uploaded before the element which appears first, ...)
The problem is that I only get 5 items (but it is because I don't want to get 100 items at once), and I don't know how to combine this with the FlatList's onEndReached (as it is a listener agent that has to be detached when component unmounts) to get more items following the same order.
Any ideas how to make this work? I have commented "<------------" on the lines of the code that I might have to change.
FIRESTORE DATABASE
Items -> user.uid -> userItems:
{
...
date: 1/1/1970
},
{
...
date: 2/1/1970
},
...
{
...
date: 31/1/1970
}
HOW MY FLATLIST HAS TO BE RENDERED:
FlatList items in order:
{ // The most recent one appears at the top of the list
...
date: 31/1/1970
},
...
{
...
date: 2/1/1970
},
{
...
date: 1/1/1970
},
CODE
const [startItem, setStartItem] = useState(null);
useEffect(() => {
const { firebase } = props;
let itemsArray = [];
// Realtime database listener
const unsuscribe = firebase // <------- With this I get the 5 most recent items when component mounts, or only one if the user has uploaded it after the component mounts
.getDatabase()
.collection("items")
.doc(firebase.getCurrentUser().uid)
.collection("userItems")
.orderBy("date") // Sorted by upload date <------------------
.startAfter(startItem && startItem.date) // <-----------------------
.limitToLast(5) // To avoid getting all items at once, we limit the fetch to 5 items <----------
.onSnapshot((snapshot) => {
let changes = snapshot.docChanges();
changes.forEach((change) => {
if (change.type === "added") {
// Get the new item
const newItem = change.doc.data();
// Add the new item to the items list
itemsArray.unshift(newItem);
}
});
// Reversed order so that the last item is at the top of the list
setItems([...itemsArray]); // Shallow copy of the existing array -> Re-render when new items added
setIsLoading(false);
// Change the start item
setStartItem(itemsArray[itemsArray.length - 1]);
});
return () => {
// Detach the listening agent
unsuscribe();
};
}, []);
...
<CardList data={items} isLoading={isLoading} onEndReached={/*how to call the function 'unsuscribe'? */} /> // <----------
What I need is to get the other next 5 more recent items when the end of the list is reached, and then, add them to the bottom of the list
UPDATE (My best approach for now)
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [start, setStart] = useState(null);
const limitItems = 5;
const getItems = () => {
/*
This function gets the initial amount of items and returns a
real time database listener (useful when a new item is uploaded)
*/
const { firebase } = props;
// Return the realtime database listener
return firebase
.getDatabase()
.collection("items")
.doc(firebase.getCurrentUser().uid)
.collection("userItems")
.orderBy("date") // Sorted by upload date
.startAt(start)
.limitToLast(limitItems)
.onSnapshot((snapshot) => {
let changes = snapshot.docChanges();
let itemsArray = [...items]; // <------- Think the error is here
console.log(`Actual items length: ${itemsArray.length}`); // <-- Always 0 WHY?
console.log(`Fetched items: ${changes.length}`); // 5 the first time, 1 when a new item is uploaded
changes.forEach((change) => {
if (change.type === "added") {
// Get the new fetched item
const newItem = change.doc.data();
// Add the new fetched item to the head of the items list
itemsArray.unshift(newItem);
}
});
// The last item is at the top of the list
setItems([...itemsArray]); // Shallow copy of the existing array -> Re-render when new items added
// Stop loading
setIsLoading(false);
// If this is the first fetch...
if (!start && itemsArray.length) {
// Save the startAt snapshot
setStart(itemsArray[itemsArray.length - 1].date);
}
});
};
const getMoreItems = () => {
/*
This funciton gets the next amount of items
and is executed when the end of the FlatList is reached
*/
const { firebase } = props;
// Start loading
setIsLoading(true);
firebase
.getDatabase()
.collection("items")
.doc(firebase.getCurrentUser().uid)
.collection("userItems")
.orderBy("date", "desc")
.startAfter(start)
.limit(limitItems)
.get()
.then((snapshot) => {
let itemsArray = [...items];
snapshot.forEach((doc) => {
// Get the new fethed item
const newItem = doc.data();
// Push the new fetched item to tail of the items array
itemsArray.push(newItem);
});
// The new fetched items will be at the bottom of the list
setItems([...itemsArray]); // Shallow copy of the existing array -> Re-render when new items added
// Stop loading
setIsLoading(false);
// Save the startAt snapshot everytime this method is executed
setStart(itemsArray[itemsArray.length - 1].date);
});
};
useEffect(() => {
// Get a initial amount of items and create a real time database listener
const unsuscribe = getItems();
return () => {
// Detach the listening agent
unsuscribe();
};
}, []);
With this code I can fetch an initial amount of items the first time, and then the next amount when I reach the end of my FlatList. But for some reason the state is not updated inside the listener... so when a new item is uploaded, all the items I got before disapears from the FlatList and they are fethed again when the end of the FlatList is reached.
Okey, after some hours coding I have found a solution. I think this is not the best because it will be better to use the onSnapshot also when the end of the FlatList is reached, but I don't know if this is possible with the Firestore's onSnapshot implementation.
The solution is based on "my best approach" code which is in the question.
Algorithm:
Just, at a first time, I create the Real-time Database Listener, which just does an onSnapshot and then call my function onItemsCollectionUpdate (passing the snapshot as argument), which can perfectly access the updated state of the app (as it is not inside the listener agent)
When we are in the onItemsCollectionUpdate, we just get the items from the snapshot and add them to the items state.
When the end of the FlatList is reached, we just call the function "getItems", which does an static retrieve of the Firestore data (I mean, using the get method from Firebase) and add it to the items state.
When component unmounts, detach the listener agent.
Related
I am trying to learn firestore realtime functionality.
Here is my code where I fetch the data:
useEffect(() => {
let temp = [];
db.collection("users")
.doc(userId)
.onSnapshot((docs) => {
for (let t in docs.data().contacts) {
temp.push(docs.data().contacts[t]);
}
setContactArr(temp);
});
}, []);
Here is my database structure:
When I change the data in the database I am unable to see the change in realtime. I have to refresh the window to see the change.
Please guide me on what I am doing wrong.
Few issues with your useEffect hook:
You declared the temp array in the way that the array reference is persistent, setting data with setter function from useState requires the reference to be new in order to detect changes. So your temp array is updated (in a wrong way btw, you need to cleanup it due to now it will have duplicates) but React is not detectign changes due to the reference to array is not changed.
You are missing userId in the dependency array of useEffect. If userId is changed - you will continue getting the values for old userId.
onSnapshot returns the unsubscribe method, you have to call it on component unMount (or on deps array change) in order to stop this onSnapshot, or it will continue to work and it will be a leak.
useEffect(() => {
// no need to continue if userId is undefined or null
// (or '0' but i guess it is a string in your case)
if (!userId) return;
const unsub = db
.collection("users")
.doc(userId)
.onSnapshot((docs) => {
const newItems = Object.entries(
docs.data().contacts
).map(([key, values]) => ({ id: key, ...values }));
setContactArr(newItems);
});
// cleanup function
return () => {
unsub(); // unsubscribe
setContactArr([]); // clear contacts data (in case userId changed)
};
}, [userId]); // added userId
const sub1 = this.angularFirestore.collection('Date').doc('username).collection('time').snapshotChanges();
const sub2 = await sub1.subscribe((snapData: DocumentChangeAction<any>[]) => {
`
// After each minute, new data is inserted in firebase storage, and the below
// code runs automatically, because I have subscribed to it.
// now the problem is, after 1 minute, snapData has duplicate records
// and for loop is also executed.
// method for getting URL from firebase storage, called from below for loop`
const send = storageRef =>
new Promise(resolve => {
storageRef.getDownloadURL().then(url => {
resolve(url);
}, err => {
}
);
}
);
for (const d of snapData) {
const storageRef =
firebase.storage().ref().child(d.payload.doc.data().timeEachMinute);
const ImgUrl = await send(storageRef);
this.timestampsList.push({
dateObj: d.payload.doc.data().dateObj,
imageUrl: ImgUrl,
date: d.payload.doc.data().screenShot,
name: d.payload.doc.data().user
});
}
}
sub2.unsubscribe();
For example, I am retrieving 1000 records, which may not be completed in one minute, suppose 500 records are retrieved, after one minute, new records start fetching from start as well as 501 onward, which lead to duplicate records. Now i am confused whether the problem is in for loop or observer subscription.
I'm using firestore and react to build my app.
This is how I add a document to the nested collection:
const handleWatch = () => {
if(watchedList.indexOf(foundProfile.id) > -1) {
alert('User already in list');
} else {
db.collection("profiles")
.doc(myProfile.id)
.collection("watched")
.add({
watchedUserId: foundProfile.id
})
.then(success => props.onWatchProfile())
}
}
And this is how I remove it:
const handleUnwatch = (id) => {
db.collection("profiles")
.doc(myProfile.id)
.collection("watched")
.where("watchedUserId", "==", id)
.onSnapshot(snapshot => snapshot.forEach(it => it.ref.delete()));
}
and I'm testing the following path:
Add a user with nick X
Delete user with nick X
Add a user with nick X
Delete user with nick X
Points 1 and 2 work perfectly.
During point 3 the user is added to the nested collection but immediately is deleted. In the firestore console, I see a blinking document.
What is the reason? Maybe it is problem because of firestore's cache?
I didn't test your code but I think this is because you don't detach the listener set in handleUnwatch. Therefore the listener is immediately triggered when you create a new doc and consequently deletes this doc (.onSnapshot(snapshot => snapshot.forEach(it => it.ref.delete()));).
The listener can be cancelled by calling the function that is returned when onSnapshot is called (doc). Depending on your exact workflow you need to call this function when necessary.
For example:
const handleUnwatch = (id) => {
const unsubscribe = db.collection("profiles")
.doc(myProfile.id)
.collection("watched")
.where("watchedUserId", "==", id)
.onSnapshot(snapshot => {
snapshot.forEach(it => it.ref.delete());
unsubscribe();
);
}
Again, the exact place where you call unsubscribe() depends on your exact functional requirements.
I want to get all posts all at once to limit requests to firebase server and increase speed. however I only want the FlatList to render 20 posts at a time from all the data saved in state for performance. Basically get all posts from firebase but render 20 at a time on device without making a request every 20 posts and then when user scrolls to the bottom more posts copied from local state to local state and now user sees 40 posts. Is that possible. Please help
This is how I am getting all posts how do make logic to limit posts locally with FlatList:
const Posts = (props) => {
//getting all posts at once but want to limit them on device from two different states
// when user scrolls down the function adds 20 more posts
const [allPosts, setAllPosts] = useState();
const [loading, setLoading] = useState(true)
useEffect(() => {
getPosts();
}, []);
const getPosts = async () => {
try {
var all = [];
const unsubscribe = await firebase
.firestore()
.collection("Posts")
.orderBy("timestamp",'desc')
.get()
.then((querySnapshot) => {
querySnapshot.docs.forEach((doc) => {
all.push(doc.data());
});
setLoading(false);
});
setAllPosts(all);
if(currentUser === null){
unsubscribe()
}
} catch (err) {
setLoading(false);
}
};
}
You can use onEndReached prop from the FlatList component to call a function when the user reaches the end of the list. Then you have to enlarge your list in your state by 20.
My application uses the eBay API and allows a user to save a list of favorite items.
I'm storing the user's Favorites data which includes the item ID for a product in a user field in my database.
I need to display the saved favorites yet with updated information from the eBay API, since the data saved (such as price, number of bids) is saved statically when a user selects a Favorite.
I've set up a separate action creator called getUpdates to the database which makes a call like this, and the call works fine.
ebayapi.com/itemID=id2342342,id3234234,id82829294,id234234234
In my Favorites component, i begin by making a call to the database to get the user's favorite items.
useEffect(() => {
props.getFavorites(props.loggedUser.id);
}, [props.loggedUser.id]);
This successfully returns and established my favorites state, which includes a list of the user's favorite items.
I'm trying to take each of the itemId's from the favorites state to pass to my ebay api to retrieve the updated data for each item.
Now, when I do something like what is shown below with a call to my getUpdates action creator...i'm getting an infinite loop, although it is storing the data properly in a new state field that I've defined favUpdates
const Favorites = props => {
useEffect(() => {
props.getFavorites(props.loggedUser.id);
}, [props.loggedUser.id]);
useEffect(() => {
setData(props.cardsToShow);
}, [props]);
const mapFAVS = props.favorites;
const data = Array.from(mapFAVS); //the favorites state is an array of objects so transforming data
const updatedFavs = data.map(item => item.id); //strips out the item id's
const formatFavs = updatedFavs.map(id => id.join(","));
props.getUpdates(formatFavs); //updates favUpdates state
I tried to change this so that the getUpdates call was within one of the useEffect hooks, like this...which also causes an infinite loop.
const Favorites = props => {
useEffect(() => {
props.getFavorites(props.loggedUser.id);
}, [props.loggedUser.id]);
useEffect(() => {
setData(props.cardsToShow);
}, [props]);
const mapFAVS = props.favorites;
const data = Array.from(mapFAVS);
const updatedFavs = data.map(item => item.id);
const formatFavs = updatedFavs.map(id => id.join(","));
useEffect(() => {
props.getUpdates(formatFavs);
}, [props]);
If i remove the second props argument the action creator does not get called and there is an axios error on the backend. Any ideas?
This was solved by adjusting the second argument in the useEffect hook.
useEffect(() => {
props.getUpdates(formatFavs);
}, [props.favorites]);