How to toggle the state of an item inside a map funtion - javascript

I'm trying to make a tag selection, the problem is, I don't know how to make a state for each item in the map, right now I have just one state, that, of course, will change all items.
That's the state and the function to toggle the state
const [selectedActivity, setSelectedActivity] = useState(false);
const toggleSelectedActivity = () => {
setSelectedActivity(!selectedActivity);
};
and that's the map function
<View style={styles.tags}>
{activitiesObject.map((data, i) => (
<TouchableOpacity
key={data.activity}
onPress={() => toggleSelectedActivity(i)}
>
<Text style={selectedActivity ? styles.selectedTag : styles.tagsText}>
{data.activity}
</Text>
</TouchableOpacity>
))}
</View>;
the image below shows what I expect to happen every time the user selects a tag
Here is the full code: https://snack.expo.io/KIiRsDPQv

You can do one of following options
change state to an array
const [selectedActivity, setSelectedActivity] = useState(Array.from({ length: activitiesObject.length }, _ => false))
const toggleSelectedActivity = (index) =>
setSelectedActivity(prev => prev.map((bool, i) => i == index ? !bool : bool))
while passing the index to function, and use selectedActivity[i] ? ...
extract
<TouchableOpacity key={data.activity} onPress={() => toggleSelectedActivity(i)}>
<Text style={selectedActivity ? styles.selectedTag : styles.tagsText}>{data.activity}</Text>
</TouchableOpacity>
to its own component, and inside it declare the state
{activitiesObject.map((data, i) => <MyComp data={data} i={i} />
const MyComp = ({ data, i }) => {
const [selectedActivity, setSelectedActivity] = useState(false)
return <TouchableOpacity key={data.activity} onPress={() => setSelectedActivity(prev => !prev)}>
<Text style={selectedActivity ? styles.selectedTag : styles.tagsText}>{data.activity}</Text>
</TouchableOpacity>
}

Related

How to add navigation to different items in a rendered array

I am attempting to press on this pressable button, and navigate to a new page. The tricky bit is that this Pressable item is part of a returned array, as there are multiple of them being rendered each with different data. I want each button to take me to a 'product page', each page being different depending on the button
Here is what i have so far:
The main function
const JobRequestList = () => {
const [data, setData] = useState([]);
useEffect(() => {
returnArray().then(data2 => {
setData(data2);
});
}, []);
if (data.length === 0) {
j = [];
return (
<ScrollView>
<View key={'ERROR'} style={styles.wrapperERROR}>
<Text style={styles.textError}> {'No Current Job Requests'} </Text>
</View>
</ScrollView>
);
} else {
return <ScrollView>{data}</ScrollView>;
}
};
This requests the data, and returns it in a form that can be rendered. It either returns a no object, or an array of items from the below function - This is where my onPress is located, and have no idea how to implement a navigation fnction into it. Please note, i already have my navigation functions setup
const returnArray = async () => {
return queryData().then(() => {
return j.map(x => {
return (
<Pressable
key={x.id}
style={styles['wrapper' + x.data().PD]}
onPress={() => {}}>
<Text style={styles.text}> {x.data().PD} </Text>
<Text style={styles.text}> {x.data().address} </Text>
<Text style={styles.text}> {x.data().time} </Text>
</Pressable>
);
});
});
};
The above function then calls the below
const queryData = async () => {
await firestore()
.collection('Jobs')
.where('driver', '==', 'TBA') //TODO ADD CUSTOMER DISTANCE
.get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
j.push(doc);
});
});
};
Here is what my navigation functions should be inside this class - Again, which is already setup correctly
const navigation = useNavigation();
navigation.navigate('JobInfo');
Thankyou in advanced!
It is anti-pattern in React to store JSX in component state. React components's rendered UI is a function of state & props. Store the data in state and then render the data mapped to JSX.
Example:
queryData fetches firebase docs & data
const queryData = async () => {
await firestore()
.collection('Jobs')
.where('driver', '==', 'TBA') //TODO ADD CUSTOMER DISTANCE
.get()
.then(querySnapshot => {
const docs = [];
querySnapshot.forEach(doc => {
docs.push({
...doc,
data: doc.data(),
});
});
return docs;
});
};
Apply the navigation logic in the Pressable component's onPress handler when mapping the data state.
const JobRequestList = () => {
const navigation = useNavigation();
const [data, setData] = useState([]);
useEffect(() => {
queryData()
.then(data => {
setData(data);
});
}, []);
return (
<ScrollView>
{data.length
? data.map(el => (
<Pressable
key={el.id}
style={styles['wrapper' + el.data.PD]}
onPress={() => {
navigation.navigate('JobInfo');
}}
>
<Text style={styles.text}> {el.data.PD} </Text>
<Text style={styles.text}> {el.data.address} </Text>
<Text style={styles.text}> {el.data.time} </Text>
</Pressable>
))
: (
<View key={'ERROR'} style={styles.wrapperERROR}>
<Text style={styles.textError}> {'No Current Job Requests'} </Text>
</View>
)
}
</ScrollView>
);
};

Accessing child state from parent

Background
I'm building an app which has at some point a FlatList which renders products. The code for the list looks like this:
<FlatList
data={data}
renderItem={({ item }) => (
<View style={styles.container}>
<View style={styles.left}>
<Text style={styles.title}>{item.name}</Text>
<Text style={styles.description}>{item.description}</Text>
<Text style={styles.price}>${item.price}</Text>
<Counter />
</View>
<Image style={styles.right} source={{uri: item.image}}/>
</View>
)}
/>
The data for this list is brought over from a Google Cloud Firestore document. Within this list you can see a component called Counter, its job is to allow the user to add and delete products from their cart. This is its code:
export default function Counter () {
const [count, setCount] = useState(0);
const handleAddition=()=>{
setCount(count + 1)
}
const handleDeletion=()=>{
{count === 0 ? setCount(count) : setCount(count - 1)}
}
return (
<View style={styles.adder}>
<TouchableOpacity onPress={() => {handleDeletion()}}>
<Text style={styles.less}>-</Text>
</TouchableOpacity>
<Text style={styles.counter}>{count}</Text>
<TouchableOpacity onPress={() => {handleAddition()}}>
<Text style={styles.more}>+</Text>
</TouchableOpacity>
</View>
)
}
Problem
As you can see from the fact that I'm rendering the counter within a FlatList, I need to keep the state stored in the child rather than in the parent, as having the count in the parent would mean that if the user selects one product, every item is added at the same time.
I need to have the a button show up when the user selects a product that allows them to navigate to their purchase summary and also I need that button to display the total cost of their selection and amount of products chosen. As you might imagine, I've no idea how to access the child's state in the parent component.
So to sum it all up:
I have a child with a state update that I need to access from its parent, but I do not know how to do it.
Question¨
Is there any way to listen to event changes in a child's state or passing it up as a prop or something like that?
Thanks a lot in advance!
Extra information
This is image shows the UI of the screen. When pressing the "+" button it updates the count +1 and it should also display a button showing the info I mentioned before.
In renderItem you can pass method callback in here
<Counter onPressFunctionItem={(isPlus) => { // handle from parent here }} />
export default function Counter ({ onPressFunctionItem }) {
const [count, setCount] = useState(0);
const handleAddition=()=>{
setCount(count + 1)
if (onPressFunctionItem) {
onPressFunctionItem(true)
}
}
const handleDeletion=()=>{
{count === 0 ? setCount(count) : setCount(count - 1)}
if (onPressFunctionItem) {
onPressFunctionItem(false)
}
}
return (...)
}
Final Output:
You don't really need to pass the child component's state to the parent to achieve the same result, you can do that very easily the conventional way.
Here is the source code of above example:
export default function App() {
const [products, setProducts] = useState(data);
/*
with this function we increase the quantity of
product of selected id
*/
const addItem = (item) => {
console.log("addItem");
let temp = products.map((product) => {
if (item.id === product.id) {
return {
...product,
quantity: product.quantity + 1,
};
}
return product;
});
setProducts(temp);
};
/*
with this function we decrease the quantity of
product of selected id, also put in the condition so as
to prevent that quantity does not goes below zero
*/
const removeItem = (item) => {
console.log("removeItem");
let temp = products.map((product) => {
if (item.id === product.id) {
return {
...product,
quantity: product.quantity > 0 ? product.quantity - 1 : 0,
};
}
return product;
});
setProducts(temp);
};
/*
this varible holds the list of selected products.
if required, you can use it as a seperate state and use it the
way you want
*/
let selected = products.filter((product) => product.quantity > 0);
/**
* below are two small utility functions,
* they calculate the total itmes and total price of all
* selected items
*/
const totalItems = () => {
return selected.reduce((acc, curr) => acc + curr.quantity, 0);
};
const totalPrice = () => {
let total = 0;
for (let elem of selected) {
total += elem.quantity * elem.price;
}
return total;
};
useEffect(() => {
console.log(products);
}, [products]);
return (
<View style={styles.container}>
<FlatList
data={products}
renderItem={({ item }) => {
return (
<Card style={styles.card}>
<View style={styles.textBox}>
<Text>{item.name}</Text>
<Text>$ {item.price.toString()}</Text>
<View style={{ flexDirection: "row" }}></View>
<View style={styles.buttonBox}>
<Button
onPress={() => removeItem(item)}
title="-"
color="#841584"
/>
<Text>{item.quantity.toString()}</Text>
<Button
onPress={() => addItem(item)}
title="+"
color="#841584"
/>
</View>
</View>
<Image
style={styles.image}
source={{
uri: item.image,
}}
/>
</Card>
);
}}
/>
<View style={{ height: 60 }}></View>
{selected.length && (
<TouchableOpacity style={styles.showCart}>
<View>
<Text style={styles.paragraph}>
{totalItems().toString()} total price ${totalPrice().toString()}
</Text>
</View>
</TouchableOpacity>
)}
</View>
);
}
You can find the working app demo here: Expo Snack

React Native, state changing for all items instead of clicked item

I created a button in flat list, when user click an specific item, it's button should change state and increment button should appear, but button changing state for all the items. I pass id too but it's not working, can someone please help me... below is my code
Items.js
<FlatList
data={this.props.items}
extraData={this.props}
keyExtractor={(items) => items.id.toString()}
numColumns={2}
renderItem={({ item }) => (
<CardBuyItem>
<Image style={styles.image} source={item.image} />
<View style={styles.detailContainer}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.subTitle} numberOfLines={1}>
{item.subTitle}
</Text>
<Text style={styles.price}>Rs {item.price}</Text>
</View>
{this.props.button && this.props.added.length > 0 ? (
<View style={styles.add}>
<Text style={styles.quantity}>{item.quantity}</Text>
<MaterialCommunityIcons
style={styles.iconUp}
size={20}
name="plus-circle-outline"
onPress={() => this.props.addQuantity(item.id)}
/>
<MaterialCommunityIcons
style={styles.iconDown}
size={20}
name="minus-circle-outline"
onPress={() => this.props.subtractQuantity(item.id)}
/>
</View>
) : (
<View style={styles.buy}>
<Text
style={styles.buyonce}
onPress={() => {
this.props.addToCart(item.id);
this.props.showCart();
this.props.showButton(item.id);
}}
>
Buy Once
</Text>
</View>
)}
</CardBuyItem>
)}
/>
const mapStateToProps = (state) => {
return {
items: state.clothes.jeans,
button: state.clothes.showButton,
added: state.clothes.addedItems,
};
};
const mapDispatchToProps = (dispatch) => {
return {
addToCart: (id) => dispatch(addToCart(id)),
addQuantity: (id) => dispatch(addQuantity(id)),
subtractQuantity: (id) => dispatch(subtractQuantity(id)),
showCart: () => dispatch(showCart()),
showButton: (id) => dispatch(showButton(id)),
};
};
That's my item list with mapStateToProsp and mapDispatchToProps here button should change it's state
reducer.js
if (action.type === SHOW_BUTTON) {
let addedItem = state.jeans.find((item) => item.id === action.id);
return {
...state,
addedItem: addedItem,
showButton: action.showButton,
};
}
const initialstate = { showButton: false}
it's my reducer function with initial state of that button
action.js
export const showButton = (id) => {
return {
type: SHOW_BUTTON,
showButton: true,
id,
};
};
it's my action where I'm describing action for my reducer
You are having a common state variable for this which causes it to show all buttons.
You can do a simple solution like this.
In your flatlist you can have a logic to display the button
{this.props.added.find(x=>x.id==item.id) !=null ? (
Or if you have to use the reducer, you will have to have a property in the array and update it which would be complex to maintain.

change Icon after clicking on it

I have a component that looks like this:
const criteriaList = [
'Nur Frauen',
'Freunde Zweiten Grades',
];
export const FilterCriteriaList: React.FunctionComponent = () => {
const [state, setState] = useState(false);
useEffect(() => {
console.log('state is,', state);
});
const myFunction = () => {
console.log('checking state', state);
if (state == false) {
setState(true);
} else {
setState(false);
}
};
return (
<View style={styles.container}>
<View style={styles.horizontalLine} />
{criteriaList.map((item: string, index: number) => (
<View key={index}>
<View style={styles.criteriaRow}>
<TouchableOpacity
onPress={() => {
myFunction();
}}>
<Icon
name="circle-thin"
color="#31C283"
size={moderateScale(20)}
/>
</TouchableOpacity>
<Text style={styles.text}>{item}</Text>
</View>
<View style={styles.horizontalLine} />
</View>
))}
</View>
);
};
Currently, I am using the circle-thin icon. I want to change it such that everytime I click on an icon, it changes to the dot-circle-o icon. Like radio buttons. However, I am not quite sure how to do so.
I thought of using ternary operators but since I am mapping my fields Idk how to setStates collectively. Maybe using the index? I don't want to make a separate state for each field. Here's a similar snack demo:
https://snack.expo.io/toTSYc2fD
I want to be able to select multiple/unselect options. I don't want to apply the same rule on all fields together.
Note: the onPress function can also be used on the Icon directly instead of the TouchableOpacity (though it is not preferred)
Using a ternary sounds like the right approach to me. Can you not do something like:
name={state ? 'dot-circle-o' : 'circle-thin'}
You could also refactor your function:
const myFunction = () => {
console.log('checking state', state);
setState(!state)
};
If you have multiple fields then there are many ways to handle it. You could call useState multiple times, eg:
const [field1, setField1] = useState(false);
const [field2, setField2] = useState(false);
You could also store all fields in the same state:
const [state, setState] = useState({field1: false, field2: false});
...
const myFunction = (fieldName) => {
console.log('checking state', state);
setState({...state, [fieldName]: !state[fieldName]})
};
I guess you'd then use the item as the "fieldName? In which case:
return (
<View style={styles.container}>
<View style={styles.horizontalLine} />
{criteriaList.map((item: string, index: number) => (
<View key={index}>
<View style={styles.criteriaRow}>
<TouchableOpacity
onPress={() => {
myFunction(item);
}}>
<Icon
name={state[item] ? 'dot-circle-o' : 'circle-thin'}
color="#31C283"
size={moderateScale(20)}
/>
</TouchableOpacity>
<Text style={styles.text}>{item}</Text>
</View>
<View style={styles.horizontalLine} />
</View>
))}
</View>
);
And to create the initial state:
const initialState = {}
criteriaList.forEach(item => initialState[item] = false)
const [state, setState] = useState(initialState);
The code would be something like below.
You have to set the index of the selected item as the state and use it to chose the icon.
const criteriaList = [
{title:'My List',checked:false},
{title:'Friends listt',checked:false},
{title:'Not Good',checked:false},
{title:'Sweet and sour',checked:false},
{title:'Automatic',checked:false},
];
export const FilterCriteriaList: React.FunctionComponent = () => {
const [state, setState] = useState(criteriaList);
useEffect(() => {
console.log('state is,', state);
});
const myFunction = (index) => {
console.log('checking state', state);
const arr=[...state];
arr[index].checked=arr[index].checked?false:true;
setState(arr);
};
return (
<View style={styles.container}>
<View style={styles.horizontalLine} />
{criteriaList.map((item: Any,index:number) => (
<View key={item}>
<View key={item} style={styles.criteriaRow}>
<Icon
style={styles.icon}
name={item.checked?"circle":"circle-thin"}
color="#31C283"
size={moderateScale(20)}
onPress= {()=>myFunction(index)}
/>
<Text style={styles.text}>{item.title}</Text>
</View>
<View style={styles.horizontalLine} />
</View>
))}
</View>
);
};

How to add multiple items to a flatlist using textinput?

i'm trying to add items to a flatlist via textinput. at the moment i can only add one item and when i try to add a second it just updates the first item, although the data from the textinput should appear below the previous textinput. i have found a few instances of a similar kind of problem and i know that i probably need to add something to my code but i just can't figure out what. below is my code. i would be grateful for any kind of help :)
function FlatlistComponent({ }) {
const listItems = [];
const [arrayHolder, setArrayHolder] = React.useState([]);
const [textInputHolder, setTextInputHolder] = React.useState('');
useEffect(() => {
setArrayHolder(listItems)
}, [])
const joinData = () => {
listItems.push({ name : textInputHolder });
setArrayHolder(listItems);
}
const FlatListItemSeparator = () => {
return (
<View
style={{
height: 1,
width: "95%",
backgroundColor: '#00678A',
alignSelf: 'center'
}} />
);
}
// Delete note
deleteNote = id => () => {
const filteredData = arrayHolder.filter(item => item.id !== id);
setArrayHolder({ data: filteredData });
}
return (
<View style={styles.MainContainer}>
<FlatList
data={arrayHolder}
width='100%'
extraData={arrayHolder}
keyExtractor={(item) => item.id}
ItemSeparatorComponent={FlatListItemSeparator}
renderItem={({ item }) => <Text style={styles.item} onPress={deleteNote(item.id)}> {item.name} </Text>}
/>
<TextInput
placeholder='Kirjoita uusi'
onChangeText={data => setTextInputHolder(data)}
style={styles.textInputStyle}
underlineColorAndroid='transparent'
clearTextOnFocus={true}
value={listItems}
/>
<TouchableOpacity onPress={joinData} style={styles.button} >
<Text style={styles.buttonText}> + </Text>
</TouchableOpacity>
</View>
);
}
listItems is always an empty array after component re-rendered, you should concat previous arrayHolder with new item:
const joinData = () => {
setArrayHolder([... arrayHolder, {name: textInputHolder }]);
// or use update function
// setArrayHolder(prev => [...prev, {name: textInputHolder }]);
}

Categories

Resources