React Native/Firebase: Issue with FlatList Re-Rendering & Duplicate Keys - firebase

I am using React Native and Firebase Realtime Database. I am experiencing two problems with the FlatList component:
I am getting a lot of "duplicate key" errors whenever the list re-renders. I am not sure why I am getting this problem because I am setting the key of every item in my list as the snap.key value generated by Firebase, which is unique (and I have verified this in my logs).
The list sometimes does not re-render, even when I scroll up or down on it. This "sometimes" behavior is throwing me off, and I have not been able to debug it. I am using the ".on" method for getting my list from the Firebase Realtime Database.
This is the code that I am using:
export default class FlatListPage extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
data: [],
};
}
makeRemoteRequest = () => {
var items = [];
DB.on('value', (snap) => {
this.getItems(snap, items);
items = items.reverse();
this.setState(
{data: items}
);
console.log(this.state.data); //checking key properties are unique
});
}
getItems = (snap, items) => {
snap.forEach((child) => {
items.push({
key: child.key,
status: child.val().status,
location: child.val().location,
});
});
}
componentWillMount(){
this.makeRemoteRequest();
}
render() {
return (
<View>
<FlatList
data={this.state.data}
renderItem={({item}) => <MyListItem item={item} />}
/>
</View>
);
}
}

You're receiving duplicate keys because you're not resetting the items array when receiving a new value event from firebase. This means you're simply re-adding the same items over and over again.
Update your makeRemoteRequest method to recreate the array each time you get a value event as follows:
makeRemoteRequest = () => {
DB.on('value', (snap) => {
var items = [];
this.getItems(snap, items);
items = items.reverse();
this.setState(
{data: items}
);
console.log(this.state.data); //checking key properties are unique
});
}
I'm not sure about number 2 - it might be that the above fixes it as a side effect.

instead of getting the whole list every time I suggest to get all elements .once and then use 'child_added' event and .limitToLast(1) to get only new items and add them to your array. Also, I noticed that you are pushing items to the array and then reversing it. You can use .unshift method that inserts items at the beginning of the array so you do not have to reverse it later.

Related

React Native state console.log

I am using firebase for my app and the data i read i want to put that in state to use it in different places.
it kinda works but when i want to console.log the state it updates like 30 times a second, am i doing something wrong?
this is my code
const db = firebase.firestore();
const [PBS1Detail, setPBS1Detail] = useState();
db.collection('Track').get().then((snapshot) => {
snapshot.docs.forEach(doc => {
renderTracks(doc)
}
)
});
const renderTracks = (doc) => {
let data = doc.data().data[0].Module;
return setPBS1Detail(data);
}
console.log(PBS1Detail)
i already tried to store it in a variable instead of state but thats not working for me, i can't get the variable from the function global
i am a noob i get it xd
You don't need a return statement when setting state. Also, it looks like you're performing some async function which means that when your component renders for the first time, PBS1Detail will be undefined since the db is a pending Promise.
You could/should put your async functions in useEffect hook:
useEffect(()=> {
db.collection('Track').get().then((snapshot) => {
snapshot.docs.forEach(doc => {
renderTracks(doc)
}
)
});
}, [])
const renderTracks = (doc) => {
let data = doc.data().data[0].Module;
setPBS1Detail(data);
}
Finally, your renderTracks function doesn't seem correct as it appears you're looping over docs and resetting your state each time.
Instead, maybe consider having an array for PBS1Detail
const [PBS1Detail, setPBS1Detail] = useState([]);
Then modify your async call:
useEffect(()=> {
db.collection('Track').get().then((snapshot) => {
let results = []
snapshot.docs.forEach(doc => {
results.push(renderTracks(doc))
}
)
setPBS1Detail(results)
});
}, [])
const renderTracks = (doc) => {
return doc.data().data[0].Module;
}
This way you're only setting state once and thus avoiding unnecessary re-renders and you're saving all of your docs instead of overwriting them.

can we run arrow function without useEffect()?

I have a function loadListings() in react native app it gets data from real time firebase database and renders it on on page in <Flatlist />
function..
let data = [];
listingRef.orderByChild("created_at").on("value", (snapshot) => {
data = [];
snapshot.forEach((listing) => {
data.push(listing.val());
});
setListings(data);
setLoading(false);
});
};
and finally
useEffect(() => {
loadListings();
}, []);
Can I use it without the effect hook?
Yes you can. Based on official documentation for useEffect hook the [] empty array at the end means what loadListings function will be executed ones when this component will be mounted.

How do I only get new documents from firebase using .onSnapshot when used with .limitToLast for pagination

I'm trying to implement a chat app with infinite scroll using Firebase. The problem is that if I don't empty my messages array when a new message is added then they're duplicated. If I empty the messages array then it doesn't keep the previous messages.
Here is the code:
getAllMessages(matchId: string) {
this.chatSvc.getAllMessages(matchId)
.orderBy('createdAt', 'asc')
.limitToLast(5)
.onSnapshot((doc) => {
if (!doc.empty) {
this.messages = [];
doc.forEach((snap) => {
this.messages.push({
content: snap.data().content,
createdAt: snap.data().createdAt,
sendingUserId: snap.data().sendingUserId,
receivingUserId: snap.data().receivingUserId
});
});
} else {
this.messages = [];
}
});
}
And the chat service that returns the reference:
getAllMessages(matchId: string): firebase.firestore.CollectionReference<firebase.firestore.DocumentData> {
return firebase
.firestore()
.collection(`matches`)
.doc(`${matchId}`)
.collection('messages');
}
I'm pushing the messages from the collection in to a messages array. If I don't add 'this.messages = []' then it will duplicate messages every time a new one is added to the collection.
How do I only get the new document from firebase with onSnapshot instead of it iterating through all of the collection again? I only want the last message because I'm going to implement infinite scroll with another query that retrieves previous messages.
Any help would be greatly appreciated.
the query will always return the last 5 result whenever a new entry that matches the condition occurs, which will create the duplicates. What you can do is to listen to changes between snapshots
getAllMessages(matchId: string) {
this.chatSvc.getAllMessages(matchId)
.orderBy('createdAt', 'asc')
.limitToLast(5)
.onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
// push only new documents that were added
if(change.type === 'added'){
this.messages.push(change.doc.data());
}
});
});
}

Chaining Firebase Firestore documents/collections

So, I have a Firestore database group like so.
companies > acme-industries > items > []
OR
collection > document > collection > document
Would it be better to just store all items inside a base collection and then add a string value to each item that defines what company it goes too? Then just query the items collection for all items linked to that company?
I am trying to retrieve the items and run them through a forEach in my firebase function. I have tried two different approaches and watched multiple videos and still am not getting results.
First Attempt Code Block
This resulted in a 500 Server Error with no explanation returned.
const itemQuerySnapshot = db.collection('companies').doc(data.userData.company).collection('items').get();
const items: any = [];
itemQuerySnapshot.forEach((doc:any) => {
console.log('doc', doc.data());
items.push({
id: doc.id,
data: doc.data()
});
});
response.json(items);
Second Attempt Code Block
This resulted in the No Such Documents! being returned
const itemRef = db.collection('companies').doc(data.userData.company).collection('items');
itemRef.get().then((doc:any) => {
if(!doc.exists) {
response.send('No such documents!');
} else {
response.send('Document Data: '+ doc.data());
}
}).catch((err:any) => {
response.status(500).send(err);
});
I am expecting something like an array of all the items to be returned from this call. I'm completely new to Firebase Firestore, what am I missing here?
UPDATE
I replaced my code with a third attempt code block and I got success with the console.log(doc.data()). However, the items object still returns empty. Is this because it's returning before the for each is done? If so, how would you prevent that to ensure every item that should be returned is?
const items: any = [];
const userRef = db.collection("companies").doc(data.userData.company);
const itemsRef = userRef.collection("items");
itemsRef
.get()
.then((snapshot: any) => {
snapshot.forEach((doc: any) => {
console.log(doc.data());
items.push({
id: doc.id,
data: doc.data()
});
});
})
.catch((err: any) => {
response.status(500).send(err);
});
response.json(items);
How would you add one more document into the mix? Say you want to get a single item. How would you do that? The following always results in Item does not exist being returned from my function.
const companyRef = db.collection('companies').doc(data.userData.company);
const itemRef = companyRef.collection('items');
const item = itemRef.where('number', '==', itemSku).get();
I must be doing something incredibly wrong here because all the videos are telling me it's incredibly easy to fetch data from Firestore. But I have yet to see that.
get returns a Promise , the callback of then function will be called once the data ready from firestore .
the line response.json(items); will be called before the items array collected correctly.
you need to move this line inside the then callback
checkout this :
.then((snapshot: any) => {
snapshot.forEach((doc: any) => {
console.log(doc.data());
items.push({
id: doc.id,
data: doc.data()
});
});
response.json(items); //items ARRAY IS READY , YOU CAN SEND YOUR RESPONSE HERE
})

Unable to render react-native Component after fetching data from firebase

I am able to fetch data from the Firebase API. I am able to set the state into the data received (I can see in Console). But when I am unable to render a Component (passing props as fetched data). Here' my code:
class NewMeals extends Component {
constructor(props){
super(props);
this.state= {
data:''
}
}
async componentDidMount(){
try{
const res = await fetch('https://frego-cb5b5.firebaseio.com/recipes.json?auth=api_key');
if(!res.ok){
throw Error(res.statusText);
}
const json = await res.json();
this.setState({data:json});
}catch(err){
console.log(err);
}
}
renderItems=()=>{
const items = this.state.data;
const arr = Object.values(items);
arr.forEach(i=>{
return <HomeMeals name={i.title} time={i.time} serve={i.serve}/>
})
}
render(){
const {mainView, CardSection, heading, } = styles;
return(
<View style={mainView}>
<Text style={heading}>New Meals This Week</Text>
<ScrollView contentContainerStyle ={CardSection} horizontal={true} showsHorizontalScrollIndicator={false}>
{this.renderItems()}
</ScrollView>
</ View>
);
}
}
I expect the HomeMeals Components will render one by one with particular names from fetched data upon calling renderItems() function. But I am getting nothing.
Any suggestions?
A couple of points here.
Do more debugging (logs).
const items = this.state.data;
const arr = Object.values(items);
console.log("items and arr", items, arr, this.state);
What values do you see from the logs above? That should give you a hint.
This one below (renderItems) doesn't work, as it doesn't return elements (or anything) to render (as you were trying to):
renderItems=()=>{
arr.forEach(i=>{
return <HomeMeals name={i.title} time={i.time} serve={i.serve}/>
})
...
What you would want is return elements (array of elements) from renderItems function like this:
renderItems=()=>{
return arr.map(i=>{
return <HomeMeals name={i.title} time={i.time} serve={i.serve}/>
})
...
Two things you will notice: (1) I added return keyword to return whatever arr.map returns. And (2) the use of arr.map vs arr.forEach. Try to figure out the reason of this on your own; why arr.map works and arr.forEach doesn't

Resources