React native single selectable components - javascript

I am trying to achieve a simple single selectable item, as shown in the image below.
Right now, I have created an array of my data items and using .map to render the components because there are only 3-4 items max, now I want to select only a single item and change the color on the basis, and if I select any other item, it should unselect all the other items but not the current single selected item/component. I tried to do this but I am able to select all of them, obviously. Below is the code:
const items = [
{
id: 1,
iconName: 'male',
title: 'Male',
name: 'male',
},
{
id: 2,
iconName: 'female',
title: 'Female',
name: 'female',
},
{
id: 3,
iconName: 'transgender',
title: 'Others',
name: 'others',
},
];
const Component = ({dispatch, iconName, title, name}) => {
const [isSelected, setIsSelected] = useState(false);
return (
<TouchableOpacity
activeOpacity={0.6}
style={
isSelected
? [styles.selectable, {backgroundColor: 'green'}]
: [styles.selectable, {backgroundColor: COLORS.PINK}]
}
onPress={() => {
setIsSelected(!isSelected);
}}>
<View style={styles.row}>
<Ionicon name={iconName} size={36} color="#fff" />
<Text>{title}</Text>
</View>
</TouchableOpacity>
);
};
const Gender = () => {
return (
<View>
<View>
<Text>Your Gender</Text>
<View>
{items.map(item => (
<Component
key={item.id}
title={item.title}
iconName={item.iconName}
/>
))}
</View>
</View>
</View>
);
};
All though I could solve this by using separate states for each button, so whenever one is selected/pressed, the other states should become false. But then I would have to render individual component without using the .map method which I find inefficient. Can someone provide any solution based on my current approach to this problem?
Thank you!

Consider moving isSelected to the parent component, and instead of storing a booolean, store the selected item id. Pass the itemId, selectedId, setSelectedId (as a callback) to the child components and change the style check to:
style={
itemId === selectedId
? [styles.selectable, {backgroundColor: 'green'}]
: [styles.selectable, {backgroundColor: COLORS.PINK}]
}
onPress={() => {
setSelectedId(itemId);
}}>
Now you can get rid of keeping track whether the item is selected in the component, and only worry about it in the context of the parent (as it should be).
const Gender = () => {
const [selectedId, setSelectedId] = useState(false);
return (
<View>
<View>
<Text>Your Gender</Text>
<View>
{items.map(item => (
<Component
key={item.id}
itemId={item.id}
selectedId={selectedId}
setSelectedId={setSelectedId}
title={item.title}
iconName={item.iconName}
/>
))}
</View>
</View>
</View>
);
};

Related

How do I display items in a nested array in react native?

I am working on a project in react native and how the program should work is that the user will start at RestaurantScreen.js and then once they pick a restaurant they will move to the next screen, MenuScreen.js. I want each restaurant to display the specific meal inside the nested array productData array in data.js. Right now the RestaurantScreen.js displays all the restaurants from the array restaurantData inside data.js but once you click on a restaurant it displays all the meals from each restaurant, but I just want the one meal, price, and image. Let me know if I need to provide move info.
data.js
export const restaurantData = [
{restaurantName:"Milan Mondayz",
businessAddress:"The Steak House of Hammond",
images:require('../images/milanpic.jpg'),
productData:[{
meal:"BBQ Chicken Plate with Sides",
foodImages:require('../images/Food1.jpg'),
price:12.00,
checked:false,id:"0"}],
id:"0"},
{restaurantName:"Nick's Cajun Flavors",
businessAddress:"123 Hammond Road",
images:require('../images/cajun.jpeg'),
productData:[{
meal:"Crawfish Etouffee",
price:13.99,
foodImages:require('../images/crawfishEto.jpeg'),
checked:false}],
id:1},
];
RestaurantScreen.js
export default function RestaurantScreen({ navigation }) {
const [indexCheck, setIndexCheck] = useState("0")
const { navigate } = useNavigation();
return (
<View style={styles.container}>
<View style={styles.cardView}>
{/*<Button
title="Go to Menu"
onPress ={()=>{navigation.navigate("Menu",{restaurantData})}}
/>*/}
<View style ={styles.headerTextView}>
<Text style ={styles.headerText}>Where Do You Want To Eat?</Text>
</View>
<View>
<FlatList
style={{marginTop:10, marginBottom:10}}
data={restaurantData}
vertical ={true}
keyExtractor={(restaurantData => restaurantData.id)}
extraData={indexCheck}
renderItem={({ item, index }) => (
<RestaurantCard
screenWidth={SCREEN_WIDTH*.94}
images={item.images}
restaurantName={item.restaurantName}
businessAddress={item.businessAddress}
//productData={item.productData}
OnPressRestaurantCard ={()=>{navigation.navigate("Menu",{index});
}}
/>
)}
showsVerticalScrollIndicator={false}
/>
</View>
</View>
</View>
)
}
MenuScreen.js
export default class MenuScreen extends Component {
render() {
const index = this.props.route.params.index
//const {meal,price,foodImages} = restaurantData[index]
return (
<View style={styles.container}>
<View style={styles.cardView}>
<View style ={styles.headerTextView}>
<Text style ={styles.headerText}>What Do You Want To Eat From?</Text>
</View>
<TouchableOpacity onPress={() => {this.props.navigation.navigate('Cart')}}>
<View style ={styles.cartContainer}>
<View style ={styles.cartCounterContainer}>
<Text style ={styles.cartText}>Cart</Text>
<View style ={styles.cartCounter}>
<Text style ={styles.cartNum}>0</Text>
</View>
</View>
</View>
</TouchableOpacity>
<View>
<View style ={{flex:1}}>
<View style ={styles.menuCard}>
<FlatList
style={{marginTop:10, marginBottom:10}}
data = {restaurantData}
keyExtractor= {item =>item.id}
renderItem = {({item,index})=>(
<MenuCard
screenWidth={SCREEN_WIDTH*.9}
images={item.foodImages}
meal ={item.meal}
price ={item.price}
onPressMenuCard={()=>{this.props.navigation.navigate("Payment")}}
/>
)}
/>
</View>
</View>
</View>
</View>
</View>
)
}
}
I can do it with SectionList and this is my example: https://snack.expo.dev/#pqv2210/exsectionlist
Just change name of field in your restaurantData: productData to data
export const restaurantData = [
{
restaurantName: 'Milan Mondayz',
businessAddress: 'The Steak House of Hammond',
images: require('../images/milanpic.jpg'),
data: [
{
meal: 'BBQ Chicken Plate with Sides',
foodImages: require('../images/Food1.jpg'),
price: 12.0,
checked: false,
id: '0',
},
],
id: '0',
},
{
restaurantName: "Nick's Cajun Flavors",
businessAddress: '123 Hammond Road',
images: require('../images/cajun.jpeg'),
data: [
{
meal: 'Crawfish Etouffee',
price: 13.99,
foodImages: require('../images/crawfishEto.jpeg'),
checked: false,
},
],
id: 1,
},
];
Replace FlatList to SectionList and modify RestaurantCard is item of list in renderItem.
const renderRestaurentInfo = ({ section }) => (
// render restaurantName, images and businessAddress
)
const renderFoodItem = ({ item }) => (
// render meal, foodImages, price
)
<SectionList
sections={restaurantData}
renderSectionHeader={renderRestaurentInfo}
keyExtractor={(_, index) => index + ''}
renderItem={renderFoodItem}
/>

Items in grid are not updating after I update the useState in react native applcation

I have a react native project. Part of my react native project is a grid that I created. When a user clicks on an item in the grid, I want to update an array that I have set in state const [selectedHomeTypes, setSelectedHomeTypes] = useState(['Houses']).
When I add an item to the array, I want it to udpate the grid so that all the items in the array have a blue background. If there was 1 item in selectedHomeTypes then 1 item in the grid has a blue background. If I add a second item to selectedHomeTypes, I want two items in the grid to have a blue background and sale with when I unselect an item that is deleted from the array.
What is hapening now is that selectedHomeTypes is being updated like normal so when an item that is not in the state is clicked on it is added to the array and same with unslelect. The issue is that the background colors of the items are not updating at all and no matter what happens, only the default item has blue background. Nothing is visually changing....
Main Screen:
const [selectedHomeTypes, setSelectedHomeTypes] = useState(['Houses'])
const updateSelectedHomeTypes = (selected) => {
let selectedHome = selectedHomeTypes
if(selectedHome.length == 0){
setSelectedHomeTypes(['Houses'])
}
if(selectedHome.includes(selected)){
let target = selectedHome.indexOf(selected)
selectedHome.splice(target, 1)
if(selectedHome.length == 0){
setSelectedHomeTypes(['Houses'])
} else {
setSelectedHomeTypes(selectedHome)
}
} else {
selectedHome.push(selected)
setSelectedHomeTypes(selectedHome)
}
}
return (
<View style={styles.filterContainer}>
<ScrollView style={styles.scrollContainer}>
<View style={styles.header}>
<Text style={styles.label}>
Filter
</Text>
<TouchableOpacity onPress={() => {resetFilter()}}>
<Text style={[styles.label, styles.blueText]}>
Reset
</Text>
</TouchableOpacity>
</View>
<View style={styles.sectionContainer}>
<FlatList
style={styles.homeTypeContainer}
data={homeTypeOptions1}
keyExtractor={(item) => item.key}
numColumns={numberOfColumns}
renderItem={(item) => {
return(
<GridItemComponent
item={item}
updateSelectedHomeTypes={updateSelectedHomeTypes}
selectedHomeTypes={selectedHomeTypes}
/>
)}}
/>
</View>
</ScrollView>
<TouchableOpacity onPress={() => {applyFilters()}}>
<View style={styles.buttonContainer}>
<Text style={styles.buttonText}>Apply Filters</Text>
</View>
</TouchableOpacity>
</View>
)
gridItemComponenet:
const GridItemComponent = (props) => {
const {
item,
updateSelectedHomeTypes,
selectedHomeTypes
} = props
return(
<>
{
selectedHomeTypes.includes(item.item.value) ? <TouchableOpacity style={styles.itemContainerBlue} onPress={() => {updateSelectedHomeTypes(item.item.value)}}>
<View style={styles.item}>
<Image style={styles.icon} source={item.item.image}/>
<Text style={styles.label}>{item.item.value}</Text>
</View>
</TouchableOpacity>
: <TouchableOpacity style={styles.itemContainer} onPress={() => {updateSelectedHomeTypes(item.item.value)}}>
<View style={styles.item}>
<Image style={styles.icon} source={item.item.image}/>
<Text style={styles.label}>{item.item.value}</Text>
</View>
</TouchableOpacity>
}
</>
)
}
list of property type options:
const numberOfColumns = 3
const homeTypeOptions1 = [
{
key: 1,
value: 'Houses',
image: require('./src/assets/home.png')
},
{
key: 2,
value: 'Condos',
image: require('./src/assets/building.png')
},
{
key: 3,
value: 'Lot/Land',
image: require('./src/assets/management.png')
},
{
key: 4,
value: 'Multi-family',
image: require('./src/assets/multi-family.png')
},
{
key: 5,
value: 'Manufactured',
image: require('.//src/assets/tiny-house.png')
},
{
key: 6,
value: 'Townhomes',
image: require('.//src/assets/townhouse.png')
}
]
as you can see in the image below, the selectedHomeTypes has 3 items in the array but only 1 item is highlighted. I am having trouble trying to update the background color of selected items dynamically
-----------UPDATE---------------------
how would I updated an array in useState if I want to eliminate an item from the array and have it rerender once the item was eliminated from the array. Keep in mind I have the index of the iteam I want to eliminate.
const updateSelectedHomeTypes = (selected) => {
let selectedHome = selectedHomeTypes
if(selectedHome.length == 0){
setSelectedHomeTypes(['Houses'])
}
if(selectedHome.includes(selected)){
let target = selectedHome.indexOf(selected)
selectedHome.splice(target, 1)
setSelectedHomeTypes(selectedHome)
} else {
setSelectedHomeTypes([...selectedHome, selected])
}
}
At a glance I think this is the problem:
When you do this:
selectedHome.push(selected)
setSelectedHomeTypes(selectedHome)
React doesn't re-render because you're setting selectedHomeTypes to the same array that's already in state.
You've pushed a new entry into it but React is doing an identity comparison between the old state and the new state to decide whether a rerender is needed. In this case newArray === oldArray so it doesn't trigger an update.
You could dodge this by spreading into a new array (and appending the new item instead of calling push):
setSelectedHomeTypes([...selectedHome, selected])
When updating state using its previous value, use the callback argument (to avoid stale state values).
https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
It should be
this.setState(state=>
({selectedHomeTypes: state.selectedHomeTypes.concat(selected)})
);
If you are updating a state property, using another state property, it's best to use class components.
State updates to arrays should be done using methods that return a new array (push does not while concat does).
See this comprehensive article to learn how to update state arrays in React.

How to send value from one tab to another tab in react native

Here in my code I am making tree tabs , on first tabe there are two input fields and buttons.
Now after entering the value in input and on button click i have to send vale to oter tabs.
Like in in name field I am entering name "Abhi" and on button click this Abhi should reflect on Tab 2.
Same like in Animal field , this Animal should reflect on third tab .
Please help
import * as React from 'react';
import { View, StyleSheet, Dimensions,Text,TextInput,TouchableOpacity } from 'react-native';
import { TabView, SceneMap } from 'react-native-tab-view';
const FirstRoute = () => (
<View style={[styles.scene, { backgroundColor: '#FFFFFF' }]} >
<View style={{}}>
<Text style={{margin:15}}>Name </Text>
<TextInput style={styles.input}
underlineColorAndroid = "transparent"
placeholder = "Name"
placeholderTextColor = "#9a73ef"
autoCapitalize = "none"
onChangeText={text => onChangeText(text)}
/>
<TouchableOpacity
style = {styles.submitButton}
onPress = {
() => this.Name()
}>
<Text style = {styles.submitButtonText}> Submit </Text>
</TouchableOpacity>
</View>
<View style={{}}>
<Text style={{margin:15}}> Favorite Animal </Text>
<TextInput style={styles.input}
underlineColorAndroid = "transparent"
placeholder = "Favorite Animal"
placeholderTextColor = "#9a73ef"
autoCapitalize = "none"
onChangeText={text => onChangeText(text)}
/>
<TouchableOpacity
style = {styles.submitButton}
onPress = {
() => this.Animal()
}>
<Text style = {styles.submitButtonText}> Submit </Text>
</TouchableOpacity>
</View>
</View>
);
const SecondRoute = () => (
<View style={[styles.scene, { backgroundColor: '#FFFFFF' }]} >
<Text> {'Name' }</Text>
</View>
);
const ThirdRoute = () => (
<View style={[styles.scene, { backgroundColor: '#FFFFFF' }]} >
<Text> {"Favorite Animal "}</Text>
</View>
);
const initialLayout = { width: Dimensions.get('window').width };
export default function TabViewExample() {
const [index, setIndex] = React.useState(0);
const [routes] = React.useState([
{ key: 'first', title: 'First' },
{ key: 'second', title: 'Second' },
{ key: 'third', title: 'Third' },
]);
const renderScene = SceneMap({
first: FirstRoute,
second: SecondRoute,
third:ThirdRoute
});
return (
<TabView
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={initialLayout}
/>
);
}
const styles = StyleSheet.create({
scene: {
flex: 1,
},
container: {
paddingTop: 23
},
input: {
margin: 15,
height: 40,
borderColor: '#7a42f4',
borderWidth: 1
},
submitButton: {
backgroundColor: '#65D370',
padding: 10,
margin: 15,
height: 40,
},
submitButtonText:{
color: 'white',
alignSelf:'center',
justifyContent:'center',
borderRadius:20
}
});
Shortest answer, is try to use a state. Using states and passing the state from parent to child may be your best option. Here is one way you can go about it.
1st in your TabViewExample add a useState() hook to keep the form data and change your renderScene() to a function, do not use SceneMap. Example:
...
const [name, setName] = React.useState(undefined);
const renderScene = ({ route }) => {
switch (route.key) {
case "first":
return <FirstRoute setName={setName} />;
case "second":
return <SecondRoute name={name} />;
case "third":
return <ThirdRoute />;
default:
<FirstRoute setName={setName} />;
}
};
(A) The reason for using renderScene() as function is explained with more detail on the "react-native-tab-view" documentation. In short when you need to pass props to components you should not use SceneMap() which you are using above instead turn renderScene into a function and use switch.
(B) We only passed setName to the first component because that's what we'll be using.
2nd - Make use of the props in your components. So now they'll look more or less like this:
const FirstRoute = props => (
<View style={[styles.scene, { backgroundColor: "#FFFFFF" }]}>
<View style={{}}>
<Text style={{ margin: 15 }}>Name </Text>
<TextInput
style={styles.input}
underlineColorAndroid="transparent"
placeholder="Name"
placeholderTextColor="#9a73ef"
autoCapitalize="none"
onChangeText={text => props.setName(text)}
/>
...
And for the SecondRoute :
const SecondRoute = props => (
<View style={[styles.scene, { backgroundColor: "#FFFFFF" }]}>
<Text> {props.name}</Text>
</View>
);
So now when you change the first Input in FirstRoute, the name state will automatically be updated, so when you go/swipe to page 2, you should see whatever you typed on the first TextInput on page 1.
PS: this is just a brief explanation so I just gave you the essential idea behind sharing data across tabs/components. On your code you can create cleaner form handler functions and handler functions for those buttons. I could've done it, but I'll leave that job for you as it was not part of your initial question. Hope this helps and let me know if you need a more detailed/in-depth response.
PS 2: If you use my current example don't click the buttons otherwise you'll get errors because you have no handler function, just type on the input and go to the second page to see the result.

Toggling classes to a specific item in a .map() function

I'm working with React Native, and I have set up a button where you can toggle the click and it'll add classes to it. However, if I have an array of items, its adding the class to all the items. I just want to add the class to the clicked button.
const Tags = props => {
const [selectTag, setSelectTag] = useState(false);
const tags = ['apples', 'oranges', 'lemon', 'watermelon', 'green peas', 'grapes'];
// toggles the click
const handleSelectTags = clicked => {
setSelectTag(clicked);
};
return (
<>
<View>
{tags.map((item, index) => (
<TouchableOpacity
onPress={() => handleSelectTags(!selectTag)}
key={index}
style={
// when selectTag is true, adds styles.selected
selectTag
? [styles.tags, styles.selected]
: [styles.tags, styles.notSelected]
}>
<Text>{item}</Text>
</TouchableOpacity>
))}
</View>
</>
);
};
const styles = StyleSheet.create({
tags: {
paddingVertical: 3,
paddingHorizontal: 9,
borderWidth: 1,
marginRight: 8,
marginBottom: 8,
borderRadius: 8,
},
selected: {
borderColor: 'green',
},
notSelected: {
borderColor: '#ccc',
},
});
export default Tags;
Not sure if what I have is the best way to do this, wondering if there's a more elegant way?
Have each TouchableOpacity manage it's own state by making it into a component. That way every time you click an item it's just updating itself :) This will allow you to have multiple clicked at the same time
const CustomComponent = props => {
let [selected, setSelected] = useState(false)
return (
<TouchableOpacity
onPress={() => setSelected(!selected)}
style={
selected
? [styles.tags, styles.selected]
: [styles.tags, styles.notSelected]
}>
<Text>{props.item}</Text>
</TouchableOpacity>
)
}
And then render this component in your map
{
tags.map((item, index) => (
<CustomComponent
key={index}
item={item}
/>
))
}
Try this if it correct
const [selectTag, setSelectTag] = useState('');
const tags = ['apples', 'oranges', 'lemon', 'watermelon', 'green peas', 'grapes'];
// toggles the click
const handleSelectTags = tag => {
setSelectTag(tag);
};
return (
<>
<View>
{tags.map((item, index) => (
<TouchableOpacity
onPress={() => handleSelectTags(item)}
key={index}
style={
// when selectTag is true, adds styles.selected
selectTag == item
? [styles.tags, styles.selected]
: [styles.tags, styles.notSelected]
}>
<Text>{item}</Text>
</TouchableOpacity>
))}
</View>
</>
);
I propose so give each button its own state.
CODE:
// each button has his own entry
const [selectTag, setSelectTag] = useState([false, false, false, false, false, false]);
const tags = ['apples', 'oranges', 'lemon', 'watermelon', 'green peas', 'grapes'];
// toggles the click
const handleSelectTags = (index) => {
var tmp = [ ...selectTag ]
tmp[index] = !selectTag[index]; // update
setSelectTag(tmp);
}
return
<>
<View>
{tags.map((item, index) => (
<TouchableOpacity
onPress={() => handleSelectTags(index)}
key={index}
style={
// update here to index of selectTag
selectTag[index]
? [styles.tags, styles.selected]
: [styles.tags, styles.notSelected]
}>
<Text>{item}</Text>
</TouchableOpacity>
))}
</View>
</>
OUTPUT
DEMO:
https://snack.expo.io/rJdZr!AHL

React Native Warning: Cannot update during an existing state transition (such as within `render`)

How do I get rid of this warning? I know I need to get rid of setState functions in the render method, but I need them, so where should I put them?
export default class List<T> extends React.PureComponent<ListProps<T>> {
state = { wrapped: false, iconName: "arrow-down" };
render(): React.Node {
const { rows, renderRow, title, onPress } = this.props;
if (this.state.wrapped === true) {
list = undefined;
this.setState({ iconName: "arrow-up" });
} else {
list = rows.map((row, index) => (
<View key={index} style={index !== rows.length - 1 ? styles.separator : {}}>
{renderRow(row, index)}
</View>
));
this.setState({ iconName: "arrow-down" });
}
return (
<TouchableWithoutFeedback>
<View style={styles.container}>
<View style={[styles.separator, styles.relative]}>
<Text style={styles.title}>{title}</Text>
<IconButton
style={styles.icon}
onPress={() => this.setState({ wrapped: !this.state.wrapped })}
name={this.state.iconName}
color="black"
/>
</View>
{list}
</View>
</TouchableWithoutFeedback>
);
}}
No, you don't need to get rid of setState calls in your render method in general. You just need to put them so that they are not called in each render call (by binding them to user events like clicks for example) and thereby trigger another re-render, that again calls setState and again re-renders and so on.
So in your particular case, you are firing setState right in the beginning in the if() { ... } else { ... } statements. No matter what this.state.wrapped is, you end up at setState.
Here is a possible solution for how you might want to change your code specifically to make it what I assume you want it to make:
export default class List<T> extends React.PureComponent<ListProps<T>> {
state = { wrapped: false };
render(): React.Node {
const { rows, renderRow, title, onPress } = this.props;
const { wrapped } = this.state;
return (
<TouchableWithoutFeedback>
<View style={styles.container}>
<View style={[styles.separator, styles.relative]}>
<Text style={styles.title}>{title}</Text>
<IconButton
style={styles.icon}
onPress={() => this.setState({ wrapped: !wrapped })}
name={wrapped ? "arrow-up" : "arrow-down"}
color="black"
/>
</View>
{!wrapped && (
<View key={index} style={index !== rows.length - 1 ? styles.separator : {}}>
{renderRow(row, index)}
</View>
)}
</View>
</TouchableWithoutFeedback>
);
}}
Because the value of your icon is directly correlated to wrapped, you don't need to specifically set the icon in the state. Rather infer it from wrapped.

Categories

Resources