FlatList items rerendering when saving the page - reactjs

Issue is when i am saving the page, the already rendered items are rerendering again, showing the error " Warning: Encountered two children with the same key, AhO8HUMnDjAH1Mh8u2jM. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version."
Here is my code
PLs note I have used pagination, and infinite scroll list methods.
import React,{useState,useEffect} from 'react';
import { View, Text, StyleSheet, FlatList, ActivityIndicator, TouchableOpacity, Button } from 'react-native';
import firestore from '#react-native-firebase/firestore';
import { Icon } from "react-native-vector-icons/Ionicons";
const HomeScreen = ({navigation}) => {
const [sessions,setSessions] = useState(new Array());
const [sessionsPerLoad] = useState(12)
const [startAfter,setStartAfter] = useState(Object)
const [lastPost,setLastPost] = useState(false)
//read docs
const getSessions = async (sessionsPerLoad) => {
const sessionArray = [];
const querySnap = await firestore()
.collection('sessions')
.orderBy('createdAt', 'desc')
.limit(sessionsPerLoad)
.get()
const lastVisible = querySnap.docs[querySnap.docs.length - 1]
querySnap.forEach((doc)=> {
let sessionData = doc.data()
sessionData.sessionID = doc.id
sessionArray.push(sessionData)
})
return {sessionArray, lastVisible}
}
const getMoreSessions = async (startAfter,sessionsPerLoad) => {
const sessionArray = [];
const querySnap = await firestore()
.collection('sessions')
.orderBy('createdAt', 'desc')
.startAfter(startAfter)
.limit(sessionsPerLoad)
.get()
const lastVisible = querySnap.docs[querySnap.docs.length - 1]
querySnap.forEach((doc)=> {
let sessionData = doc.data()
sessionData.sessionID = doc.id
sessionArray.push(sessionData)
})
return {sessionArray, lastVisible}
}
useEffect(()=>{
getSession()
},[])
const getSession = async () => {
const sessionsData = await getSessions(sessionsPerLoad)
setSessions([...sessions, ...sessionsData.sessionArray])
// console.log('Sessions',sessions)
setStartAfter(sessionsData.lastVisible)
// console.log('Last VIsible',sessionsData.lastVisible)
}
const getMoreSession = async () => {
if(!lastPost){
const sessionsData = await getMoreSessions(startAfter,sessionsPerLoad)
setSessions([...sessions, ...sessionsData.sessionArray])
// console.log('More Session',sessions)
setStartAfter(sessionsData.lastVisible)
sessionsData.sessionArray.length==0 ? setLastPost(true):setLastPost(false)
}
}
const RenderCard = ({item})=>{
return(
<TouchableOpacity onPress={()=>{navigation.navigate('HomeScreen2', {item})}}>
<View style={{padding: 10}}>
<Text>Title= {item.title}</Text>
<Text>Description= {item.description}</Text>
</View>
</TouchableOpacity>
)
}
return(
<View>
<FlatList
data={sessions}
renderItem={({item})=><RenderCard item={item} />}
keyExtractor={(item)=>item.sessionID}
onEndReached={getMoreSession}
onEndReachedThreshold={0.01}
scrollEventThrottle={150}
ListFooterComponent={()=>
!lastPost && <ActivityIndicator />}
/>
</View>
);
};
const styles = StyleSheet.create({
});
export default HomeScreen

Pls ignore this issue, in the development phase, this will happen.
To ignore these warnings use -> https://stackoverflow.com/a/67670955/13733008

Related

React - Getting collection from Firebase and updating state to render a child component

pardon my ignorance but I'm still in the early days of learning react and firebase.
I'm trying to implement some functionality where a user can click on a category, then they will be navigated to this ViewItemPage component, where I'm attempting to get a collection from firebase based on the category that the user clicked. Once the collection has been retrieved I'm trying to select a random item from the collection and use that data to render another child component.
I'm assuming this is either a lifecycle issue and/or I'm using react hooks completely incorrectly, but I've been stuck trying to figure this out way too long
import { collection, query, where, getDocs } from 'firebase/firestore';
import { db } from '../firebase';
import { useLocation } from 'react-router';
import { AnotherComponent } from './AnotherComponent ';
export function ViewItemPage() {
const { state } = useLocation();
const { category } = state;
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedItem, setSelectedItem] = useState();
const itemsRef = collection(db, 'items');
const getItems = async (e) => {
try {
const q = query(itemsRef, where('category', '==', category));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
setItems({ data: doc.data() });
});
setLoading(false);
} catch (err) {
console.log(err.message);
}
};
const getRandomItem = () => {
const randomItem = items[Math.floor(Math.random() * items.length)];
setSelectedItem(randomItem);
};
useEffect(() => {
getItems();
// eslint-disable-next-line
}, [loading]);
useEffect(() => {
getRandomItem();
// eslint-disable-next-line
});
return <AnotherComponent category={category} item={selectedItem} />;
}
in the above example I'm attempting to use a forEach loop to update the state, but I've also tried mapping the response:
setItems(
querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id }))
);
and it has the same result, the child component seems to be passed and empty object, and throws an error when attempting to read any of the items properties
After a bit more learning, trial and error, and refactoring it seems to be working as I intended it to now. Here's the code in case someone else might find it useful
import React, { useState, useEffect, useRef } from 'react';
import { collection, query, where, getDocs } from 'firebase/firestore';
import { db } from '../firebase';
import { useLocation } from 'react-router';
import { AnotherComponent} from './AnotherComponent ';
export function ViewItemPage() {
const { state } = useLocation();
const { category } = state;
const [loading, setLoading] = useState(true);
const [randomItem, setRandomItem] = useState();
let itemArray = [];
const getItems = async () => {
try {
const q = query(
collection(db, 'items'),
where('category', '==', category)
);
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
itemArray = [...itemArray, doc.data()];
});
setRandomItem(
itemArray[Math.floor(Math.random() * itemArray.length)]
);
setLoading(false);
} catch (err) {
console.log(err.message);
}
};
useEffect(() => {
getItems();
// eslint-disable-next-line
}, [loading]);
if (loading) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
}}
>
Loading the data
</div>
);
}
return <AnotherComponent item={randomItem} />;
}

Firestore data in a flatlist with react native

I've been trying to output data from my Firestore db to a Flatlist in React native but so far unsuccesfull.
I've used this Flatlist for RN and Firestore docs as reference to getting started but for some reason I am missing something here regarding the output method of the Flatlist because it wont output the flatlist itself. When I console log the locations array it shows me all the docs inside that i've queried so it does push them all into an array and my understanding is of FlatLists that they need an array to function but it does not throw any error just doesn't render. Any help more than welcome!
useEffect(async () => {
const locations = [];
const querySnapshot = await getDocs(collection(db, "Location"));
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
locations.push(doc.data());
console.log(locations);
});
return () => querySnapshot();
}, []);
return (
<View style={styles.screen}>
<Text>hello</Text>
<FlatList data={locations}
renderItem={({ item }) => (
<View >
<Text>name: {item.name}</Text>
<Text>Depth: {item.depth}m</Text>
<Text>GeoLocation: {item.geo}</Text>
<Text>id: {item.uid}</Text>
</View>
)}
/>
Your variable locations is defined in your useEffect. The FlatList can not access it. You need to create a state via useState and store the data in there once it is loaded. Setting the state will cause a rerendering of the component and the FlatList will be updated with the new data.
Here is one possible implementation.
const SomeScreen = () => {
const [locations, setLocations] = useState([])
useEffect(() => {
const loadData = async () => {
const querySnapshot = await getDocs(collection(db, "Location"));
setLocations(querySnapshot.map(doc => doc.data()))
}
loadData()
}, [setLocations]);
return (
<FlatList data={locations}
renderItem={({ item }) => (
...
)}
/>
)
}
Here is a simple way:
import React, {useState} from 'react';
import { Button, View, Text, FlatList } from 'react-native';
import {query, where, collection, getDocs } from 'firebase/firestore';
import {db} from './firestoreconfig.js';
const App = () => {
const [cities, setCities] = useState([]);
async function readData()
{
setCities([]);
const mycities = [];
const q = query(collection(db, "cities"), where("capital", "==", true) );
const querySnapshot = await getDocs(q);
querySnapshot.forEach( (city) => {mycities.push({key: city.id, name: city.data().name} )})
setCities(mycities);
}
return (
<View
style = {{marginTop: 50}}
>
<Button
title='Read Data'
onPress={ () => readData()}
/>
<FlatList
data={cities}
renderItem = {
({item}) =>
<Text>{item.key} {item.name}</Text>
}
/>
</View>
);
}
export default App;

How to implement infinite list using React Query in react native with Flatlist

Here is my code:
import React, {useState} from 'react';
import {useQuery, useInfiniteQuery} from 'react-query';
import {getMeetup} from '../../api/methods/getMeetups';
export default function MyFunction(props) {
const [next, setNext] = useState('');
const fetchData = async ({pageParam = ''}) => {
const response = await getMeetup( pageParam);
console.log('API RESP', response);
return response;
};
const {data, isLoading, fetchNextPage} = useInfiniteQuery(
'myData',
fetchData,
{
getNextPageParam: (lastPage, pages) => lastPage?.next?._id,
},
);
console.log('RQUERY CHECK', data);
const getMore = () => {
console.log('data end', data?.pages[0]?.next?._id);
fetchNextPage({pageParam: data?.pages[0]?.next?._id});
};
const flattenData = data?.pages
? data?.pages?.flatMap((page) => [...page.Docs])
: [];
return (
<View>
<FlatList
style={{
marginBottom: verticalScale(40),
paddingHorizontal: scale(15),
}}
data={flattenData}
keyExtractor={(item) => item._id}
renderItem={({item, index}) => {
return <ListItem data={item} index={index} />;
}}
onEndReachedThreshold={0.1}
onEndReached={getMore}
/>
</View>
);
}
The problem i am facing is when the page loads the api calls one by one with unique next ids or page param. What i was trying to implement is , when user reaches the end of the page (onEndReached) the data needs to be fetched with a new page param.
getNextPageParam return the value for the next page. So you don't need pass pageParam in fetchNextPage unless you want overwrite, for any reason, the next page value.
You can add a hasNextPage validation for unnecessary requests.
const {data, isLoading, hasNextPage, fetchNextPage} = useInfiniteQuery( /* your code */ )
const getMore = () => {
if(hasNextPage)
fetchNextPage();
};

rendering content dynamically in react native not working

I am trying to render content dynamically. I have an array of JSON data that I loop over, and, for now (testing purposes), create a text node "hey" for each element of that array,pushing it to another array called renderedData. I am trying to render the contents of this array, renderedData, but it is not working. I have no idea why - nothing is rendered, yet when I try a dummy array that is declared on the spot, such as "test", it works.
import * as React from 'react';
import { StyleSheet, Button, TextInput, Modal, Alert, TouchableHighlight, SafeAreaView, SectionList, FlatList, ListView } from 'react-native';
import EditScreenInfo from '../components/EditScreenInfo';
import { Text, View } from '../components/Themed';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import AsyncStorage from '#react-native-community/async-storage';
import { useState } from 'react';
import { parse } from '#babel/core';
import { getInitialURL } from 'expo-linking';
const payments: any[] = []
export default function PaymentsScreen(){
const getData = async () => {
try {
const keys = await AsyncStorage.getAllKeys();
const result = await AsyncStorage.multiGet(keys)
return result
} catch(e) {
console.log(e)
}
}
React.useEffect(() => {
getData()
})
const data: (string | null)[] = []
const renderedData: string[] = []
async function parseData(){
const payments = await getData()
if (payments != null && payments != undefined) {
console.log(payments)
payments.map((eachPayment: any[]) => {
if (eachPayment[1] != null && eachPayment[1].includes("{"))
data.push(eachPayment[1])
})
}
data.map((eachPayment: any) => {
if (eachPayment != null) renderedData.push("hey")
})
return data
}
parseData()
return (
<View>
{data.map((info: any) => <Text>{info}</Text>)}
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginHorizontal: 16
},
item: {
backgroundColor: "#f9c2ff",
padding: 20,
marginVertical: 8
},
header: {
fontSize: 32,
backgroundColor: "#fff"
},
title: {
fontSize: 24
}
});
Since you have async call, here is how i would do it :
import React, { Component } from "react";
import { render } from "react-dom";
import "./style.css";
const App = () => {
const fakeApiData = [1, 2, 3];
const [data, setData] = React.useState([]);
const [renderedData, setRenderedData] = React.useState([]);
const getApiDataAsync = () => Promise.resolve(fakeApiData);
// Equal to componentDiMount()
React.useEffect(() => {
// This is your async call to get your data
const data = getApiDataAsync().then(data => setData(data));
}, []);
// Effect triggered after data changes, so when you get your data
React.useEffect(() => {
const futurRenderedData = [];
data.map(item => futurRenderedData.push(item));
setRenderedData(futurRenderedData);
}, [data]);
return (
<div>
{renderedData.map(item => <span>{item} </span>)}
</div>
);
};
render(<App />, document.getElementById("root"));
The idea is to get your data, be sure to have it, either through a promise, an await, or an observable, and then setting your renderedData.
Here is your code modified, tell me if there is still something wrong :
export default function PaymentsScreen() {
const [data, setData] = React.useState(new Array<string>());
const [renderedData, setRenderedData] = React.useState(new Array<string>());
const getData = async () => {
try {
const keys = await AsyncStorage.getAllKeys();
const result = await AsyncStorage.multiGet(keys);
return result;
} catch (e) {
console.log(e);
}
};
async function parseData() {
const payments = await getData();
if (payments) {
payments.foreach((eachPayment: any[]) => {
if (eachPayment[1] && eachPayment[1].includes('{'))
data.push(eachPayment[1]);
});
}
data.map((eachPayment: any) => {
if (eachPayment != null) setRenderedData('hey');
});
return data;
}
React.useEffect(async () => {
setData(await parseData());
}, []);
return (
<View>
<View>
{data.map((info: any) => (
<Text>{info}</Text>
))}
</View>
<View>
{renderedData.map(item => <span>{item} </span>)} {/*If this appear in the html then it works*/}
</View>
</View>
);
}

TypeError: undefined is not a function (near "...dataSource.map...")

I am trying to render the API data onto the page but get this error
TypeError: undefined is not a function (near "...dataSource.map...")
Unrelated Question: Also I am new to mobile app development and am wondering when you get data from an API is best practice to store it in a database or render it directly onto a page?
import React, { Component, useState, useEffect } from "react";
import { View, Text, StyleSheet, ActivityIndicator } from "react-native";
import { ScreenContainer } from "react-native-screens";
export const Home = () => {
const [isLoading, setisLoading] = useState(true);
const [dataSource, setdataSource] = useState(null);
useEffect(async () => {
const response = await fetch(
"https://facebook.github.io/react-native/movies.json"
);
const data = await response.json();
const movies = data.title;
setdataSource(movies);
setisLoading(false);
// .catch((error)) => {
// console.log(error)
// }
}, []);
if (isLoading) {
return (
<View>
<ActivityIndicator />
</View>
);
} else {
let moviest = dataSource.map((val, key) => {
return (
<View key={key}>
<Text>{val}</Text>
</View>
);
});
return (
<ScreenContainer style={styles.container}>
<View>{moviest}</View>
</ScreenContainer>
);
}
You have to check dataSource is null or undefined before map.
let moviest = dataSource && dataSource.map((val, key) => {
Please check type of dataSource.
When I check https://facebook.github.io/react-native/movies.json data, type of data.title is string("The Basics - Networking")
Maybe you should use movies.
const data = await response.json();
const movies = data.movies;
setdataSource(movies);
setisLoading(false);

Resources