I'm trying to display a list of forums the a user has joined into a flatlist, which is firstly retrieved from firestore via an onSnapshot function in my useLayoutEffect as shown below:
useLayoutEffect(() => {
const unsubscribe = onSnapshot(userRef, (snapshot) => {
const joinedForums = [];
if (snapshot.data().myForums.length > 0) {
snapshot.get("myForums").forEach((forumDoc) => {
let docRef = doc(db, "forums", forumDoc);
getDoc(docRef).then((docInf) => {
joinedForums.push({
...docInf.data(),
id: docInf.id,
});
});
});
}
setJoinedForums(joinedForums);
});
return () => unsubscribe();
}, []);
Whenever I load the screen, the flatlist is empty.
Here is the code for my flatlist:
<FlatList
data={joinedForums}
renderItem={({ item }) => (
<TouchableOpacity
onPress={() => {
navigation.navigate("ForumDetails", {
forumId: item.id,
});
}}
>
<Card>
<View>
<Text>{item.id}</Text>
<Text>Title: {item.title}</Text>
<Text>Desc: {item.desc}</Text>
<Text>Created By: {item.createdBy}</Text>
</View>
{/* Maybe add a recent message component onSnapshot for each forum */}
<TouchableOpacity
onPress={() => {
leaveForum(item.id);
}}
>
<Text>Leave</Text>
</TouchableOpacity>
</Card>
</TouchableOpacity>
)}
/>
Here is a screenshot of the screen:
After having initially researched on the issue, I discovered that updating a state would cause the flatlist to re-render, so I have tried to manually trigger an update to a separate dummy state through a button which then results for in the flatlist displaying the items correctly.
Here is a screenshot of the screen upon manually updating state:
I'm not sure where to tweak my code to get the flatlist to render the items on first load, without having to manually trigger a state change.
You should update the state in onSnapshotafter rendering new data retrieved from the snapshot.
Related
my current issue with my react native app is that when a user wants to open a lesson (from the lessons array with each object being a lesson with a title,description,img url etc)to make it bigger through a modal, its state does not update. What i Mean by this is that the books title,description,and other attributes won't change if you press on a new lesson. What would be the solution to this?
export default function Learn() {
const [modalVisible, setModalVisible] = useState(false);
const [lessons,setLessons] = useState()
useEffect(() => {
async function data() {
try {
let todos = []
const querySnapshot = await getDocs(collection(db, "lessons"));
querySnapshot.forEach((doc) => {
todos.push(doc.data())
});
setLessons(todos)
console.log(lessons)
}
catch(E) {
alert(E)
}
}
data()
}, [])
return (
<View style={learnStyle.maincont}>
<View>
<Text style={{fontSize:28,marginTop:20}}>Courses</Text>
<ScrollView style={{paddingBottom:200}}>
{lessons && lessons.map((doc,key) =>
<>
<Modal
animationType="slide"
transparent={true}
visible={modalVisible}
onRequestClose={() => {
Alert.alert("Modal has been closed.");
setModalVisible(!modalVisible);
}}
>
<View style={styles.centeredView}>
<View style={styles.modalView}>
<Image source={{
uri:doc.imgURL
}} style={{width:"100%",height:300}}/>
<Text style={{fontWeight:"700",fontSize:25}}>{doc.title}</Text>
<Text style={{fontWeight:"700",fontSize:16}}>{doc.desc}</Text>
<Pressable
style={[styles.button, styles.buttonClose]}
onPress={() => setModalVisible(!modalVisible)}
>
<Text style={styles.textStyle}>Hide Modal</Text>
</Pressable>
</View>
</View>
</Modal>
<LessonCard setModalVisible={setModalVisible} title={doc.title} desc={doc.desc} img1={doc.imgURL} modalVisible={modalVisible}/>
</>
)}
<View style={{height:600,width:"100%"}}></View>
</ScrollView>
</View>
</View>
)
}
What it looks like:
**image 1 is before you press the modal and the 2nd one is after
**the main issue though is that if you press cancel and press on another lesson the modal that opens has the the same state(title,imgurl,anddesc) as the first lesson and does not change.
The problem is that you create a lot of modal windows through the map function, I suggest making one window and passing the key as a parameter and using it to search for a specific array of data that is shown to the user (photo, title, etc.)
The problem is that all 3 Modals are controlled by the one state variable. So when the code sets modalVisible to true, all 3 modals are being opened at once.
You can fix this in a few ways, but a simple way would be to move the Modal and its state into the LessonCard component. This way each modal will have its own state that's only opened by its card. So the loop in Learn will just be:
{lessons && lessons.map((doc,key) => (
<LessonCard lesson={doc} key={key} />
)}
Adding to address question in comments
LessonCard should not accept setModalVisible or modalVisible props. The
const [modalVisible, setModalVisible] = useState(false);
should be inside LessonCard, not Learn. That way each Card/Modal pair will have its own state.
Additionally, although React wants you to pass the key into LessonCard in the map function, LessonCard should not actually use the key prop for anything. See https://reactjs.org/docs/lists-and-keys.html#extracting-components-with-keys
So, the LessonCard declaration should just be something like
export default function LessonCard({lesson}) {
I am getting the below errors when I tried to fetch array data from firebase firestore.
How can I solve this issue could someone please suggest me the code structure.
VM22 bundle.js:3927 Warning: Each child in a list should have a unique "key" prop.
Check the render method of CellRenderer. See https://reactjs.org/link/warning-keys for more information.
at http://localhost:19006/static/js/bundle.js:157320:19
at CellRenderer (http://localhost:19006/static/js/bundle.js:172017:36)
import React, { useState, useEffect } from 'react';
import {View, Button, Text, FlatList, StyleSheet, Pressable, TouchableOpacity} from 'react-native'
import {firebase} from '../config';
const Testing = ({ navigation }) =>{
const [users, setUsers] = useState([]);
const todoRef = firebase.firestore().collection('testing');
useEffect(() => {
todoRef.onSnapshot(
querySnapshot => {
const users = []
querySnapshot.forEach((doc) => {
const { ArrayTesting
} = doc.data()
users.push({
id: doc.id,
ArrayTesting
})
})
setUsers(users)
}
)
}, [])
return (
<View style={{ flex:1,}}>
<FlatList
data={users}
numColumns={1}
renderItem={({item}) => (
<Pressable >
<View>
<View>
{ (item.ArrayTesting)
? item.ArrayTesting.map((item) => <Text>{item}</Text>)
: <Text></Text>
}
</View>
</View>
</Pressable>
)} />
</View>
);}
export default Testing;
While you are mapping components in react, you have to provide unique key for the components, so it will know which component to rerender.
<View>
{ (item.ArrayTesting)
// if item has unique field, please provide that as key in Text
? item.ArrayTesting.map((item,index) => <Text key={index}>{item}</Text>)
: <Text></Text>
}
</View>
Hey flatlist already has a property called keyExtractor , please use that , so that all elements already have an unique key and you dont have to pass any explicitly to child comps :
<Flatlist
keyExtractor={(item, _index) => String(_index)}
/>
And also in your renderItem add the below :
{ (item.ArrayTesting)
? item.ArrayTesting.map((item,index) => <Text key={index}>{item}</Text>)
: <Text></Text>
}
Hope it helps, feel free for doubts
The above answer is correct, however, I'd like to add a point of clarity that the answer above even touched on a bit. I'd like to emphasize it a bit more :).
When adding a key prop to each element (so that react knows how to keep track of them in the DOM), we should provide each <Text /> component with a proper unique key.
This could be like an id: 1 property returned on each object or data point from the server. This will ensure headaches of React misunderstanding your data since your keys are just ordered indexes. If ever your data changes (you update your list to remove one item) React might remove the wrong item do to shifting indexes.
<View>
{(item.ArrayTesting)
// if item has unique field, please provide that as key in Text
? item.ArrayTesting.map((item) => <Text key={item.id}>{item}</Text>)
: <Text></Text>
}
</View>
Im trying to pass a function handleNewFavourite (which updates my favouriteList state array) from my HomeScreen to my DetailsScreen via navigation params but Im getting the following error: Non-serializable values were found in the navigation state
How should I pass functions that modified the state between different stack screens?
HomeScreen code:
<FlatList
data={checkCategory()}
renderItem={({item}) => (
<TouchableOpacity
onPress={() =>
navigation.navigate('Details', {
item,
handleNewFavourite,
})
}>
<LessonCard lesson={item} />
</TouchableOpacity>
)}
/>
DetailScreen code:
const LessonDetails = ({lesson, handleNewFavourite}: LessonProps) => {
const [favourite, setFavourite] = useState<boolean>(lesson.favourite);
return (
<LessonDetailsContainer>
<LessonDetailsInfoContainer>
<LessonDetailsCategoryHead>
<LessonDetailsCategory>{lesson.category}</LessonDetailsCategory>
<TouchableOpacity
onPress={() => {
setFavourite(!favourite);
handleNewFavourite(lesson);
}}>
<LessonDetailsFavouriteIcon>
{favourite ? '❤️' : '🤍'}
</LessonDetailsFavouriteIcon>
</TouchableOpacity>
</LessonDetailsCategoryHead>
<LessonDetailsTitle>{lesson.title}</LessonDetailsTitle>
<LessonDetailsAuthor>{lesson?.author}</LessonDetailsAuthor>
</LessonDetailsInfoContainer>
<LessonDetailsCardImage
source={{
uri: lesson.image,
}}
/>
<LessonDetailsContentContainer>
<LessonDetailsDescriptionText>
{lesson.content}
</LessonDetailsDescriptionText>
</LessonDetailsContentContainer>
</LessonDetailsContainer>
);
};
export default LessonDetails;
For situation like this, you should learn global state management. ( Context API - Redux etc. )
I think you are disrupting in the wrong way the parameters passed to DetailScreen it should be something like this:
const LessonDetails = ({route}: LessonProps) => {
const {lesson, handleNewFavourite} = route.params;
// The rest of your component here
}
As the documentation here suggested. But as #omerfarukose mentioned is not a bad idea to use state management in this particular scenario
Swiping gestures i.e. scrolling, pull to refresh, etc.
I'm pulling my hair out trying to make this work, I don't understand how apps like Facebook accomplish this so well. It seems like such a simple thing to implement but I cannot for the life of me figure it out.
FYI: I'm using a FlatList with Touchable components inside. I've been messing around with the FlatList props (on scroll, on scroll begin drag, on scroll end drag, etc) as well as the Touchable props (on press, on press in, on press delay in, etc).
What I want: On the Facebook app, the MOMENT I begin scrolling or pulling to refresh, tap feedback is disabled so it doesn't look like I clicked on a post. But at the same time the MOMENT I tap on a post, the tap feedback is super responsive. How is this done?
What I get: The moment I begin scrolling or pulling to refresh, the tap feedback is played even though I wanted to scroll/refresh. To fix this, I tried putting a pressDelayIn of 50ms. But now, quickly tapping on a post doesn't play the feedback.
App.js
export default function App() {
const [refreshing, setRefreshing] = useState(false);
const [posts, setPosts] = useState([
{
id: 1,
username: '#somedude',
body: 'This is the best app ever, wow.',
},
{
id: 2,
username: '#doggo',
body: 'Woof woof. Woof woof woof! Woof... Woof woof? Woof!',
},
]);
const onRefresh = () => {
setRefreshing(true);
setTimeout(() => setRefreshing(false), 1000);
}
return (
<SafeAreaView style={styles.container}>
<FlatList
data={posts}
renderItem={({ item }) => <Post post={item} />}
keyExtractor={item => item.id}
refreshing={refreshing}
onRefresh={onRefresh}
/>
</SafeAreaView>
);
}
Post.js
export const Post = ({ post }) => {
return (
<TouchableOpacity
activeOpacity={0.5}
onPress={() => console.log(`Press id ${post.id}`)}
>
<View style={styles.postPontainer}>
<View style={{ marginBottom: 5 }}>
<Text>{post.username}</Text>
</View>
<View style={styles.textContainer}>
<Text>{post.body}</Text>
</View>
</View>
</TouchableOpacity>
);
}
I definitely understand the frustration. I'd +1 ucup's suggestion to checkout react-native-gesture-handler. In the mean time, I disabled the TouchableOpacity while scrolling, and dialed back the delayPressIn and it seem to work pretty well. See what you think:
Add state to track canPress
export default function App() {
const [canPress, setCanPress] = useState(true); //
const [refreshing, setRefreshing] = useState(false);
...
Wire up to the FlatList
<SafeAreaView style={styles.container}>
<FlatList
data={posts}
onScrollBeginDrag={() => setCanPress(false)} //
onScrollEndDrag={() => setCanPress(true)} //
renderItem={({item}) => <Post post={item} canPress={canPress} />} //
keyExtractor={item => item.id}
refreshing={refreshing}
onRefresh={onRefresh}
/>
</SafeAreaView>
Then just wire up to your TouchableOpacity
<TouchableOpacity
disabled={!canPress} //
activeOpacity={0.5}
delayPressIn={50} //
onPress={() => console.log(`Press id ${post.id}`)}>
...
Good luck!
you can check this answer i think it will help you to resolve your problem.
I think pointerEvents is the property you need to handle this.
this property controls whether the View can be the target of touch events.
Reference 1
Reference 2
I'm attempting to build a To-Do list feature for an application; however, I'm struggling to get it right.
Basically, I want a user to be able to create new list items via the "submit" key while typing, or a button (both already done). When the new list item is created, I want the corresponding TextInput within the FlatList to be automatically focused. This works...up to a certain point. Once I create 10 list items, the behavior stops working properly and the focus is no longer on the next, newly created FlatList item.
I've figured out that if I change the "initialNumToRender" property in the FlatList and increase it past 10, it will solve the problem, temporarily, until reaching that cap. However, I don't want to reduce the performance of my FlatList and I want to find a different solution. It seems to me that the entire FlatList is re-rendering past 10 items and the TextInput loses focus, but I haven't been able to grasp yet how to focus on the TextInputs created after the 10th row.
The FlatList in question:
<FlatList
key="flatlist"
ListHeaderComponent={listHeader()}
initialNumToRender={10}
data={listItems}
// TODO: Will need to change to use actual IDs at some point as opposed to just indexing
renderItem={({item, index}) => (
<>
<View style={listStyles.itemView}>
<Pressable onPress={() => radioButtonPressed(index)}>
<MainListRadioButton completed={getRadioButtonState(index)} />
</Pressable>
{listItems[index].text.length > 0 ? (
<TextInput
style={listStyles.itemText}
placeholder={`Item ${index + 1}`}
defaultValue={listItems[index].text}
value={listItems[index].text}
onChangeText={text => listItemTextChanged(text, index)}
ref={index === listItems.length - 1 ? lastListItemRef : null}
onSubmitEditing={() => addListItem()}
/>
) : (
// Text input for "empty" list items so that a backspace can delete the entire row.
<TextInput
style={listStyles.itemText}
placeholder={`Item ${index + 1}`}
defaultValue={listItems[index].text}
value={listItems[index].text}
onChangeText={text => listItemTextChanged(text, index)}
ref={index === listItems.length - 1 ? lastListItemRef : null}
onKeyPress={({nativeEvent}) => {
if (nativeEvent.key === 'Backspace') {
deleteListItem(index);
}
}}
/>
)}
</View>
<View style={styles.divider} />
</>
)}
/>
How I'm implementing the behavior to shift focus to the next row so far:
useEffect(() => {
if (lastListItemRef.current) {
lastListItemRef.current.focus();
}
}, [listItems]);
You'd probably be better off using scrollToIndex() along with useLayoutEffect since you're wanting to trigger the scroll on a visual change.
useLayoutEffect(() => {
flatlistRef.current?.scrollToIndex(listItems.length - 1)
}, [flatlistRef, listItems])
Rather than controlling your TextInput focus at the parent level, you could turn your listItem function into a ListItem component. By doing this, you can store the ref to each new ListItem component within each component instance, and on mount of each component, focus it.
So your DraggableFlatList declaration becomes something like this:
<DraggableFlatList
...
renderItem={({ item, index, drag, isActive }) => (
<ListItem
item={item}
index={index}
drag={drag}
isActive={isActive}
addListItem={addListItem}
deleteListItem={deleteListItem}
completed={getRadioButtonState(index)}
text={listItems[index].text}
onChange={listItemTextChanged}
radioButtonPressed={radioButtonPressed}
/>
)}
...
/>;
And your ListItem component definition looks something like this:
const ListItem = ({
item,
index,
drag,
isActive,
text,
completed,
onChange,
radioButtonPressed,
addListItem,
deleteListItem,
} = props) => {
// Create ref to this TextInput
const textRef = useRef(null);
// On mount, set the TextInput focus
useEffect(() => {
if (textRef.current) {
textRef.current.focus();
}
}, []);
return (
<>
<Pressable style={listStyles.itemView} onLongPress={drag}>
<Pressable onPress={() => radioButtonPressed(index)}>
<MainListRadioButton completed={completed} />
</Pressable>
<TextInput
ref={textRef}
style={listStyles.itemText}
placeholder={`Item ${index + 1}`}
defaultValue={text}
value={text}
onChangeText={(text) => onChange(text, index)}
onSubmitEditing={() => addListItem()}
onKeyPress={({ nativeEvent }) => {
if (nativeEvent.key === 'Backspace') {
deleteListItem(index);
}
}}
/>
</Pressable>
<View style={listStyles.divider} />
</>
);
};
I've updated your snack here as a POC: https://snack.expo.dev/TZk_48CHF
you need to know if listItems is not a empthy array, for that reason you need to create this function isFechet() to evaluate it.
useEffect(() => {
const goToFocus=await ()=>{
if (lastListItemRef.current) {
lastListItemRef.current.focus();
}
}
if(isFechet(listItems)){
goToFocus()
}
}, [listItems]);
Thanks everyone. I actually ended up figuring it out myself. The steps I took were the following:
Completely remove setRef and ref within the TextInput and component.
Add autoFocus=true as a property to the TextInput. Previously I had a typo without capitalizing the F.
Change onTextChange to onEndEditing so the List Items only update after submitting text as opposed to on each text change.
In this case you have to use the flatlist scrollToIndex function along with viewPosition property properly for the selected changes.Also you have to use the onScrollToIndexFailed prop in flatlist along with setTimeout.
viewPosition: 0.5 ---> put the selected item in middle,0 and 1 are other values
const const scrollToIndex = (ref, index = 0) => {
if (ref && ref.current && index > -1) {
ref.current.scrollToIndex({ index, viewPosition: 0.5 });
}
};
const flatListRef = useRef();
useEffect(() => {
const selectedIndex = getSelectedIndex();
setTimeout(() => {
scrollToIndex(flatListRef, selectedIndex);
}, 50);
}, [title, flatListRef]);
<FlatList
ref={flatListRef}
initialNumToRender={categoryFilters.length + 1}
onScrollToIndexFailed={info => {
if (flatListRef !== null && info.index > -1) {
setTimeout(
() =>
flatListRef.current.scrollToIndex({
index: info.index,
animated: true,
viewPosition: 0.5,
}),
50
);
}
}}
refreshing={false}
data={listItems}
listKey={(_, index) => index.toString()}
renderItem={()=>{}}
/>