I am creating an app that allows a user to select multiple items from a Flatlist and the state changes once selected. However when I move away from that screen, all the selected items go back to the unselected state. How can use Firebase to save that state so it doesn't revert when I leave the screen? I am also open to alternative solutions to this issue.
Thank you for your time.
export default function StoreCatalogue({ navigation }) {
const { data, status } = useQuery('products', fetchProducts)
const [catalogueArray, setCatalogueArray] = useState([])
const [isSelected, setIsSelected] = useState([])
const [addCompare, setAddCompare] = useState(false)
const storeToDB = async (item) => {
if (!addCompare) {
await db.collection('users').doc(auth.currentUser.uid).collection('myProducts').doc(item.storeName + item.genName).set({
product_id: item.id,
product_genName: item.genName
})
} else {
await db.collection('users').doc(auth.currentUser.uid).collection('myProducts').doc(item.storeName + item.genName).delete()
}
}
const clickHandler = async (item) => {
setAddCompare(
addCompare ? false : true
)
if (isSelected.indexOf(item) > -1) {
let array = isSelected.filter(indexObj => {
if (indexObj == item) {
return false
}
return true
})
setIsSelected(array)
} else {
setIsSelected([
...isSelected, item
])
}
}
return (
<View style={styles.container}>
<FlatList
extraData={isSelected}
keyExtractor={(item) => item.id}
data={catalogueArray}
renderItem={({ item }) => (
<TouchableOpacity style={styles.btn} onPress={() => { storeToDB(item); clickHandler(item) }}>
<MaterialCommunityIcons name='plus-circle-outline' size={24} color={isSelected.indexOf(item) > -1 ? 'grey' : 'green'} />
<View style={{ position: 'absolute', bottom: 3 }}>
<Text style={{ fontSize: 10, textAlign: 'center', color: isSelected.indexOf(item) > -1 ? 'grey' : 'green' }}>{isSelected.indexOf(item) > -1 ? 'item \nAdded' : 'Add to\n Compare '}</Text>
</View>
</TouchableOpacity>
)}
/>
</View>
)
}
The problem is when you leave a screen the state gets reset to original, you can use Redux to store the state separately and on an event store the information on firebase once.
Firebase can be costly for this operation since every read and write will be chargeable.
Related
I have been trying to create a search bar all day. I finally found this guide which seemed ok: https://blog.jscrambler.com/add-a-search-bar-using-hooks-and-flatlist-in-react-native/. I followed it through using my own API and I am not getting any errors exactly, but the code in this tutorial seems unfinished.
Here is what I have:
import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, SafeAreaView, TextInput } from 'react-native';
import { Card, Header } from 'react-native-elements'
import { styles } from './styles.js';
import filter from 'lodash.filter';
const FormsScreen = ({navigation, route}) => {
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState([]);
const [query, setQuery] = useState('');
const [fullData, setFullData] = useState([]);
//Fetch all users from database
useEffect(() =>{
setIsLoading(true);
fetch('http://10.0.2.2:5000/forms').then(response =>{
if(response.ok){
return response.json();
}
}).then(data => setFullData(data)).then(setIsLoading(false));
}, []);
function renderHeader() {
return (
<View
style={{
backgroundColor: '#fff',
padding: 10,
marginVertical: 10,
borderRadius: 20
}}
>
<TextInput
autoCapitalize="none"
autoCorrect={false}
clearButtonMode="always"
value={query}
onChangeText={queryText => handleSearch(queryText)}
placeholder="Search"
style={{ backgroundColor: '#fff', paddingHorizontal: 20 }}
/>
</View>
);
}
const handleSearch = text => {
const formattedQuery = text.toLowerCase();
const filteredData = filter(fullData, form => {
return contains(form, formattedQuery);
});
setData(filteredData);
setQuery(text);
};
const contains = ({ ID }, query) => {
console.log("ID was: "+ID);
console.log("Query was: "+query);
const id = ID;
console.log('id was: '+id);
if (id.toString().includes(query)) {
return true;
}
return false;
};
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" color="#5500dc" />
</View>
);
}
else{
return (
<SafeAreaView>
<Header
leftComponent={{ icon: 'menu', color: '#fff' }}
centerComponent={{ text: 'Request Forms', style: { color: '#fff', fontSize: 25} }}
rightComponent={{ icon: 'home', color: '#fff' }}
/>
<FlatList
ListHeaderComponent={renderHeader}
keyExtractor={(item) => item.ID.toString() }
data={fullData}
renderItem={({item}) => (
<Card>
<Card.Title>{item.ID}</Card.Title>
<Card.Divider/>
<View style={styles.Container}>
<Text>{item.Comments}</Text>
{/* <Image source={require('./System apps/Media Manager/Gallery/AppPhotos/45e5cefd-7798-4fe9-88de-86a0a15b7b9f.jpg')} /> */}
<Text>{item.RoadName}</Text>
</View>
<View style={styles.ListContainer}>
<Text style={styles.LabelText}>Name</Text>
<Text style={styles.LabelText}>Phone</Text>
<Text style={styles.LabelText}>Email</Text>
</View>
<View style={styles.ListContainer}>
<Text style={styles.CardText}>{item.Name}</Text>
<Text style={styles.CardText}>{item.Phone}</Text>
<Text style={styles.CardText}>{item.Email}</Text>
</View>
</Card>
)}
/>
</SafeAreaView>
);
}
}
export default FormsScreen;
I have 2 main problems here.
1.) The tutorial had me initialize data and setData. setData is called and it looks to me like that is the final result after the search. The problem is that the author never actually used the variable data so I what do I do with it? Right now the the list is unaltered no matter what happens.
2.) I know he is using a different API so instead of filtering through First name, Last name, and Email I am only searching through ID. In this section of the tutorial:
const contains = ({ name, email }, query) => {
const { first, last } = name;
if (first.includes(query) || last.includes(query) || email.includes(query)) {
return true;
}
return false;
};
How does this code relate first and last to the first and last name values in the data? When I use this code but substitute name with ID and therefor first with id the value of query is correct (19 for example) but the value of ID is 2040 which is not the value I am looking for, but 2040 is the last ID in the database, or in other words the most recently entered row.
This is a sample of my data for reference:
Any help is greatly appreciated.
Please update
data={fullData}
to
data={query ? data : fullData} in flat list props. That should display your filtered data whenever search query updated.
We have a feature where users can discuss in 3 different topics. This topics are displayed with a flatlist:
<View style={{ position: 'absolute', left: 0, top: 0 }}>
<FlatList
horizontal
data={categories}
extraData={
selectedCategory // for single item
}
style={styles.flatList}
renderItem={({ item: rowData }) => (
<TouchableOpacity
onPress={() => setSelectedCategory(rowData)}
style={rowData === selectedCategory ? styles.selected : styles.unselected}
>
<Text style={{ fontWeight: 'bold', color: '#FFFFFF', padding: 6 }}>
{ rowData }
</Text>
</TouchableOpacity>
)}
keyExtractor={(item, index) => item.toString()}
/>
</View>
our issue is when we change topics, the data is rendering prematurely, thus rendering the wrong category data several times. a screen grab here can show you the issue better that I can explain it in words: https://imgur.com/a/koCa6Pr
Once it stops, it might settle and display the correct topic data, or it might show another topics data (such as moves when you are on memes).
Here is our code to display a topic's data:
useEffect(() => {
getCollection();
}, [selectedCategory]);
const getCollection = async() => {
setIsLoading(true);
const index = 1;
const getThoughtsOneCategory = firebase.functions().httpsCallable('getThoughtsOneCategory');
return getThoughtsOneCategory({
index,
category: selectedCategory,
}).then((result) => {
setThoughts(result.data);
setIsLoading(false);
}).catch((err) => {
console.log(err);
});
};
and our results: https://imgur.com/a/koCa6Pr
any idea what we can do to make sure the data being displayed is the correct topic/category, and to stop the flickering?
making the call to firebase asynchronous fixed the issue:
const getThoughtsOneCategory = await firebase.functions().httpsCallable('getThoughtsOneCategory');
After getting data from API I set it to state, and render items in Flatlist,
when I select any item from it I manipulate data and add a new property to item object named as "toggle: true"
and it's works well when I select any item from list I add a border based on toggle,
But when I go back to previous screen then re open the lists screen I can see the border rendered around the items, although I reset the state when the unmounted screen
So what's the wrong I made here?
Code snippet
Data
export default {
...
services: [
{
id: 0,
name: 'nameS0',
logo:
'https://cdn2.iconfinder.com/data/icons/hotel-98/64/hair-dryer-tools-beauty-hairdressing-512.png',
price: 19.99,
},
],
employees: [
{
id: 0,
name: 'name0',
img:
'https://www.visualelementmedia.com/wp-content/uploads/2015/04/person-4-400x629.jpg',
},
...
],
};
const VendorProfile = ({navigation}) => {
const [services, setServices] = React.useState(null);
const [employees, setEmployees] = React.useState(null);
const [serviceSelected, setServiceSelected] = React.useState(null);
const [employeeSelected, setEmployeeSelected] = React.useState(null);
// For selected Item (services, employees)
const itemSelected = (data, id) => {
const updated = data.map((item) => {
item.toggle = false;
if (item.id === id) {
item.toggle = true;
data === services
? setServiceSelected(item)
: setEmployeeSelected(item);
}
return item;
});
data === services ? setServices(updated) : setEmployees(updated);
};
...
const renderEmployees = ({item}) => {
return (
<TouchableOpacity
onPress={() => itemSelected(employees, item.id)}
delayPressIn={0}
style={styles.employeeContainer}>
<EmployeePattern style={{alignSelf: 'center'}} />
<View style={styles.employeeLogo}>
<Image
source={{uri: item.img}}
style={[styles.imgStyle, {borderRadius: 25}]}
/>
</View>
<View style={{marginTop: 30}}>
<Text style={{textAlign: 'center'}}> {item.name}</Text>
</View>
<View style={{marginTop: 10, alignSelf: 'center'}}>
{item.toggle && <AntDesign name="check" size={25} color="#000" />} // here it's stuck after back and reopen the screen
</View>
</TouchableOpacity>
);
};
React.useEffect(() => {
setServices(VendorProfileData.services);
setEmployees(VendorProfileData.employees);
() => {
setServices(null);
setEmployees(null);
};
}, []);
return (
<View style={styles.container}>
<FlatList
data={services}
renderItem={renderServices}
horizontal
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={{
justifyContent: 'space-between',
flexGrow: 1,
}}
/>
.....
</View>
);
};
Ok so after trying multiple times, i got it
change this
const updated = data.map((item) => {
to this
const updated = data.map((old) => {
let item = {...old};
and please make sure everything is working and we didn't break a thing :),
On your ItemSelected function you are passing the whole employees list, and going through it now thats fine, but when you changing one item inside this list without "recreating it" the reference to that item is still the same "because its an object" meaning that we are modifying the original item, and since we are doing so, the item keeps its old reference, best way to avoid that is to recreate the object,
hope this gives you an idea.
I have Flatlist with an array of items and when I select an item, all other items take the state/color of the selected item grey. How do I ensure that only selected items changes state?
Also the items I select are stored in a firestore database and are deleted when unselected. Can you also help me to store this changed state into firestore db. Thank you and I appreciate your effort.
const [catalogueArray, setCatalogueArray] = useState([])
const [addCompare, setAddCompre] = useState(false)
const [textvalue, setTextValue] = useState(`Add to \n Cart`)
const [textColor, setTextColor] = useState('green')
const storeToDB = async (item) => {
if (!addCompare) {
await db.collection('users').doc(auth.currentUser.uid)
.collection('myProducts').doc(item.storeName + item.genName)
.set({
product_id: item.id,
product_genName: item.genName
})
} else {
await db.collection('users').doc(auth.currentUser.uid)
.collection('myProducts').doc(item.storeName + item.genName).delete()
}
}
const clickedBtn = () => {
setAddCompre(!addCompare ? true : false)
setTextValue(!addCompare ? `Item \n Added` : `Add to \n Cart`)
setTextColor(!addCompare ? 'grey' : 'green')
}
render(
....
<FlatList
keyExtractor={(item) => item.id}
data={catalogueArray}
renderItem={({ item }) => (
......
<TouchableOpacity style={styles.btn} onPress={() => { storeToDB(item); clickedBtn() }}>
<MaterialCommunityIcons name='plus-circle-outline' size={24} color={textColor} />
......
<Text style={...}>{textvalue}</Text>
</TouchableOpacity>
/>
const [isSelected, setIsSelected] = useState([])
const [addCompare, setAddCompare] = useState(false)
const catalogueList = () => {
const catalogue = data.filter(item => {
return item.storeName == navigation.state.params.selectStore
}).map(item => ({ ...item }))
setCatalogueArray(catalogue)
}
const storeToDB = async (item) => {
if (!addCompare) {
await db.collection('users').doc(auth.currentUser.uid).collection('myProducts').doc(item.storeName + item.genName).set({
product_id: item.id,
product_genName: item.genName
})
} else {
await db.collection('users').doc(auth.currentUser.uid).collection('myProducts').doc(item.storeName + item.genName).delete()
}
}
const clickedBtn = async (item) => {
setAddCompare(
addCompare ? false : true
)
if (isSelected.indexOf(item) > -1) {
let array = isSelected.filter(indexObj => {
if (indexObj == item) {
return false
}
return true
})
setIsSelected(array)
} else {
setIsSelected([
...isSelected, item
])
}
}
return (
....
<FlatList
extraData={isSelected}
keyExtractor={(item) => item.id}
data={catalogueArray}
renderItem={({ item }) => (
<View style={styles.contain}>
<TouchableOpacity style={styles.btn} onPress={() => { storeToDB(item); clickedBtn(item) }}>
<MaterialCommunityIcons name='plus-circle-outline' size={24} color={isSelected.indexOf(item) > -1 ? 'grey' : 'green'} />
<View style={{ position: 'absolute', bottom: 3 }}>
<Text style={{ fontSize: 10, textAlign: 'center', color: isSelected.indexOf(item) > -1 ? 'grey' : 'green' }}>{isSelected.indexOf(item) > -1 ? 'item \nAdded' : 'Add to\n Compare '}</Text>
</View>
</TouchableOpacity>
</View>
)}
/>
)
}```
I'm working on a react-native mobile app, which displays feeds which users can comment on. Against each feed, a textbox and a submit is available where users can comment and by default, each submit button is disabled till it receives a value.
The problem I'm currently facing is when a user starts to type for example in the first textbox, all send buttons in each row becomes active and also after the data is sent to the server the textbox still remains active.
Screen.js
const feedsScreen = ({ posts, showLoader, onCommentPost, onSetComment, isEnabled }) => (
<View style={styles.container}>
{posts.length === 0 && showLoader === false ? (
<View style={styles.noPost}>
<Text
style={{
textAlign: 'center',
fontSize: 20,
color: '#36a',
fontFamily: 'HelveticaNeue-Light',
fontWeight: '500'
}}
>
No Posts for this group yet
</Text>
<Image source={require('../../../Images/CC/post.png')} />
</View>
) : null}
{posts.map((item, i) => {
return (
<View key={i} style={styles.user}>
<Text style={{ marginBottom: 10, fontSize: 16, color: '#778899' }}>{item.text}</Text>
<TextInput
onChangeText={onSetComment}
label="Write Comment"
underlineColor="#36a"
style={{ backgroundColor: '#fff', width: '90%' }}
/>
<View style={{ alignSelf: 'flex-end', position: 'relative', right: 0, top: -20 }}>
<Icon
disabled={!isEnabled}
iconStyle={[isEnabled === true ? styles.likedColor : styles.unLikedColor]}
name="md-send"
type="ionicon"
color="#999"
onPress={() => {
onCommentPost(item);
}}
/>
</View>
</View>
);
})}
</View>
);
export default feedsScreen;
Screen_Container.js
import React, { Component } from 'react';
import { Platform, StyleSheet, Text, View } from 'react-native';
import feedsScreen from './feedsScreen';
import * as API from '../../../API';
class FeedsContainerContainer extends Component {
state = {
posts: [],
userId: '',
showLoader: true,
showArchiveSnackBar: false,
showAfterCommentSnackBar: false,
comment: ''
};
componentDidMount = () => {
const { navigation } = this.props;
this.setState(
{userId: navigation.getParam('communityMemberId')},
() => this.getData()
);
};
setComment = comment => {
this.setState({ comment: comment });
};
getData = async () => {
const { userId } = this.state;
const data = await API.getGroupPost(userId);
console.log(data);
this.setState({ posts: data, showLoader: false });
};
commentPost = async item => {
this.setState({
posts: this.state.posts.map(post =>
post.id === item.id ? { ...post, modalLoader: true } : post
)
});
const { userId, communityId, groupId, comment } = this.state;
const data = await API.commentPost(userId, item.id, comment);
this.setState({
posts: this.state.posts.map(post =>
post.id === item.id ? { ...post, modalLoader: false } : post
)
});
this.setState(prevState => ({
posts: prevState.posts.map(el => {
if (el.id === item.id) {
return {
...el,
commentsCount: el.commentsCount + 1
};
}
return el;
})
}));
this.setState({ comment: '' });
};
render() {
const isEnabled = this.state.comment.length > 0;
return (
<feedsScreen
posts={this.state.posts}
showLoader={this.state.showLoader}
onCommentPost={this.commentPost}
modalLoader={this.state.modalLoader}
onSetComment={this.setComment}
isEnabled={isEnabled}
commentValue={this.state.comment}
userId={this.state.userId}
/>
);
}
}
export default FeedsContainerContainer;
How do I only make the active textbox submit button active when it has a value and also clears it after its value sent to the database
There can be multiple post at same time so you should save comment for each post
Step 1 : Change in state . Save comment as object with key as index/id of each post
state = {
comment: {}
};
Step 2: Change onChangeText={onSetComment} function and also pass index of item
<TextInput
onChangeText={(comment) => onSetComment(comment,i)}
.......
/>
Step 3: Save comment with item index
setComment = (comment,index) => {
this.setState({ comment:...this.state.comment, [index]:comment });
}
Step 4: Remove isEnabled prop from main component
isEnabled={isEnabled}
Step 5 :check enable state in each item as you are sending commentValue prop receive in Screen.js
const feedsScreen = ({commentValue, posts, showLoader, onCommentPost, onSetComment, isEnabled }) => (
......
{posts.map((item, i) => {
const isEnabled = commentValue[i] && commentValue[i].length > 0;
.........