Hi everyone I want when I click that the heart fills up and becomes red or it is empty and is black, only I can't achieve this result, neither the color nor the shape of the heart changes when I click on it. Can you help me?
function Discussion(props) {
const [heartTouched, setHeartTouched] = useState(new Array(DATA.length));
const NewArray = (id, value) => {
let array = heartTouched;
array[id] = value;
setHeartTouched(array);
console.log(heartTouched);
};
return (
<FlatList
style={{ backgroundColor: "#F3D26F" }}
data={DATA}
extraData={DATA}
keyExtractor={({ id }) => id.toString()}
renderItem={({ item, index }) => (
<Screen style={styles.container}>
<View style={styles.commentContainer}>
<View style={styles.textCommentContainer}>
<Text>{item.user}</Text>
<Text style={styles.comment}>Hello</Text>
</View>
<TouchableOpacity
style={styles.heartContainer}
onPress={
index === item.id - 1
? heartTouched[index]
? () => NewArray(index, false) + (item.like -= 1)
: () => NewArray(index, true) + (item.like += 1)
: null
}
>
<MaterialCommunityIcons
style={styles.heartStyle}
name={heartTouched[index] ? "heart" : "heart-outline"}
color={heartTouched[index] ? "red" : "black"}
size={24}
/>
</TouchableOpacity>
</View>
</Screen>
)}
/>
);
}
The problem mostly is from the line
let array = heartTouched;
When you need to change the state in react you have to do it in an immutable way,
meaning that you can't take the array and change one element, you have to recreate a new one, then change the new one.
let array = [...heartTouched];
React only trigger rerender when the state (if it was an array or object), is recreated, because it looks at the reference not the object, and when you change one element you are not changing the reference.
This way you are creating a new array from the state, hope this works for you.
Related
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.
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>
}
Until expo lets me use Realm databases ive decided to go with Json and asyncstorage. The source data originates from Json anyway so for this app it makes sense to leave it as json.
I have a flatlist with the json displayed. To the left of each item in the list is a star icon.
When I touch each item in the list the star will go solid to indicate it has been pressed. Press it again, the icon will be an outline of a star to indicate it has been de-selected.
The onpress function looks like this symbols is the name of the JSON data the JSON data has 3 keys... symbol, name and iconName. Symbol is the item in the flatlist that is touched.
onPressListItem = ( symbol ) => {
for (var i = 0; i < this.state.symbols.length; i++){
if (this.state.symbols[i].symbol === symbol){
const copyOfSymbolsList = [...this.state.symbols];
if (copyOfSymbolsList[i].iconName === 'md-star') {
copyOfSymbolsList[i].iconName = 'md-star-outline';
} else {
copyOfSymbolsList[i].iconName = 'md-star';
}
this.setState({ symbols:copyOfSymbolsList });
}
}
}
So as you can see above it basically just scrolls through the entire json array to find the appropriate row and then makes a copy of the list, changes the data and then sets state again.
The app isn't super fast maybe half a second delay before the icon changes on my pixel 2 and there is only 100 records in the list. Im a little worried if the list gets into the thousands of rows itll be really bad.
Is there a better/faster/simpler/more react-y way to solve this problem?
EDIT
This is the renderListItem function which calls the onPress function
renderListItem = ({ item }) => {
return (
<TouchableOpacity
onPress={() => this.onPressListItem(item.symbol)}
>
<View style={{flex: 1, flexDirection: 'row'}}>
<View style={{backgroundColor: 'powderblue'}}>
<Ionicons style={styles.listItemIcon} name={item.iconName} />
</View>
<View style={{backgroundColor: 'skyblue'}}>
<Text style={styles.listItem}>
{item.name.toUpperCase()} {item.symbol}
</Text>
</View>
</View>
</TouchableOpacity>
);
};
EDIT #2
This is the FlatList code.
<View style={styles.mainContainer}>
<FlatList
data={this.state.symbols}
keyExtractor= {(item, index) => item.symbol}
ItemSeparatorComponent={this.renderListSeparator}
renderItem={this.renderListItem}
/>
</View>
i guest your json structure is like this.
the_data = [
{'iconName': 'xxx', 'symbol': '<i class="fa fa-items"></i>'}
]
You can access this structure by index, passing the index to the onPressListItem function.
renderListItem = ({ item }, index) => {
return (
<TouchableOpacity
onPress={() => this.onPressListItem(index)}
>
<View style={{flex: 1, flexDirection: 'row'}}>
<View style={{backgroundColor: 'powderblue'}}>
<Ionicons style={styles.listItemIcon} name={item.iconName} />
</View>
<View style={{backgroundColor: 'skyblue'}}>
<Text style={styles.listItem}>
{item.name.toUpperCase()} {item.symbol}
</Text>
</View>
</View>
</TouchableOpacity>
);
};
With this design you don't have to iterate your json.
onPressListItem = ( index ) => {
the_dataobj = the_data[index];
the_dataobj.iconName = 'md-start';
if (the_dataobj.iconName === 'md-start'){
the_dataobj.iconName = 'md-start_outline';
}
//you don't need a extra else here, is in-necesarry
the_data[index] = the_dataobj;
//set state ... do more
}
BTW: This is have nothing to do with react, the correct design of your workflow should be independent of the framework library.
Happy Codding!!!
For those of you reading, the accepted answer did not update the rendered list properly on screen. So the actual working code for me is below. Im pretty sure its because the accepted answer did not use the this.setState function to write the values back to the array.
Also the if then else needs to have an else in it because when the user taps the same row twice i want it to reverse its changes.
All that being said, the updates are still very slow. About the same speed as it was when using the for loop in the question. Not sure why.
onPressListItem = ( index ) => {
const copyOfSymbolsList = [...this.state.symbols];
thePressedSymbol = copyOfSymbolsList[index];
if (thePressedSymbol.iconName === 'md-star') {
thePressedSymbol.iconName = 'md-star-outline';
}else{
thePressedSymbol.iconName = 'md-star';
}
copyOfSymbolsList[index] = thePressedSymbol;
this.setState({ symbols:copyOfSymbolsList });
}
I'm using react native's powerful flatlist this is my array which is like this:
data = [
{Color:[{'label':'BLUE','value':'10'}, {'label':'RED','value':'11'}]},
{Size:[{'label':'SMALL','value':'13'},{'label':'BIG','value':'12'}]}
]
and here it Is my flatlist
<FlatList data={data}
numColumns={1}
keyExtractor={(item, index) => index.toString()}
extraData={this.state}
ItemSeparatorComponent={FlatListItemSeparator}
renderItem={({item, index}) => {
return(
<View >
<Text >{Object.keys(item)}</Text>
<FlatList data={item[Object.keys(item)]}
numColumns={5}
keyExtractor={(index) => "D"+index.toString()}
extraData={this.state}
renderItem={({item, index}) => {
return(
<TouchableOpacity onPress={() => this._handleclick(index, item)} style={[
styles.Buttoncolor, this.state.selected == index+item.label
? styles.onfocusbutton
: null
]}>
<Text style={[styles.textcolor,this.state.selected == index+item.label?styles.white:null]}>{item.label}</Text>
</TouchableOpacity>
)
}
}/>
</View>)}}/>
this is the function where im matching the index of item..
_handleclick = (index, item) => {
this.setState({selected: index+item.label, selectedColor: item.value})
}
here it is what I tried :->
I call hand click function match its value with index+label name and it get change itself but
when I click on BLUE it change its color but when I click on SMALL
it can change itself but blue gets its previous condition
now here's main what I want to do :->
I want to select only one item based on there array which is 'Color'
I want that when I click on BLUE it can be change it color .
and if I click also on SMALL it can also change color itself that two value need to be stored.
I hope you could easily understand.
thanks in advance
UPDATE 1
STATUS :SOLVED
Changes
_handleclick = (index, item,objectname) => {
const {selected,result} = this.state
let selectedIndex = this.state.selected.indexOf(item.value);
if(selectedIndex == -1){
let my = this.state.result.filter((e)=>{return e[objectname]})
selected.forEach((items) => {
let arrayofattribute = result.filter((e)=>{return e[objectname]})
arrayofattribute.forEach((value,index)=>{
newval = value[objectname].map((val,i)=>{
if(items == val.label)
{
selected.splice(selected.indexOf(val.label),1)
}
})
})
})
this.setState({selected:[item.label,...this.state.selected]})
}
}
in Flatlist render
<TouchableOpacity
onPress={() => this._handleCategoryColor(index, item,objectname)}
style={[
styles.Buttoncolor,
this.state.selected.indexOf(item.label) != -1
? styles.onfocusbutton
: null,
]}>
<Text
style={[
styles.textcolor,
this.state.selected.indexOf(item.label) != -1
? styles.white
: null,
]}>
{item.label}
</Text>
</TouchableOpacity>
you can try array instead of storing single selected item,
make selected state as empty array when you are declaring.
...
<TouchableOpacity onPress={() => this._handleclick(index, item)} style={[
styles.Buttoncolor, this.state.selected.indexOf(index+item.label) != -1
? styles.onfocusbutton
: null
]}>
<Text style={[styles.textcolor, this.state.selected.indexOf(index+item.label) != -1?styles.white:null]}>{item.label}</Text>
</TouchableOpacity>
here is you handle method, push new selected item to selected array
_handleclick = (index, item) => {
let selectedIndex = this.state.selected.indexOf(index+item.label);
if(selectedIndex== -1){
this.setState({selected: [...this.state.selected,index+item.label], selectedColor: item.value})
}
}
Probably the issue is with the function arguments assignments. If you see both renderItem methods in Flatlist uses renderItem={({item, index}) so the last renderItem can access the above item, index. There's an confusion when onPress called, which item or index to put in the callback.
Please try to add the arg different names. Like
<FlatList data={data}
numColumns={1}
keyExtractor={(item, index) => index.toString()}
extraData={this.state}
ItemSeparatorComponent={FlatListItemSeparator}
renderItem={({item, index}) => {
return(
<View >
<Text >{Object.keys(item)}</Text>
<FlatList data={item[Object.keys(item)]}
numColumns={5}
keyExtractor={(index) => "D"+index.toString()}
extraData={this.state}
renderItem={({set, key}) => {
return(
<TouchableOpacity onPress={() => this._handleclick(key, set)} style={[
styles.Buttoncolor, this.state.selected == index+item.label
? styles.onfocusbutton
: null
]}>
<Text style={[styles.textcolor,this.state.selected == index+item.label?styles.white:null]}>{item.label}</Text>
</TouchableOpacity>
)
}
}/>
</View>)}}/>
Let me know if this fixes your issue.
Happy coding. :)
If I correcty understood you need to select just one option for each property (colors and sizes). So you need to have a field in your state for each element you want to be saved.
Your _handleClick function could be something like this:
_handleclick = (index, item) => {
const { Color } = this.state.data[0];
const { Size } = this.state.data[1];
this.setState(state => ({
selectedColor: Color.find(color => color.label === item.label)
? index + item.label
: state.selectedColor,
selectedSize: Size.find(size => size.label === item.label)
? index + item.label
: state.selectedSize,
}));
};
Every time you click on an element you need to know if it's a color or a size and then correctly update the right field. Maybe this is not the most performant way, but it works.
Then, in your render method, in order to assign style you could check both conditions:
<TouchableOpacity
onPress={() => this._handleclick(index, item)}
style={[
styles.Buttoncolor,
this.state.selectedSize == index + item.label ||
this.state.selectedColor == index + item.label
? styles.onfocusbutton
: null,
]}>
<Text
style={[
styles.textcolor,
this.state.selectedSize == index + item.label ||
this.state.selectedColor == index + item.label
? styles.white
: null,
]}>
{item.label}
</Text>
</TouchableOpacity>
I tried to reproduce your question in this snack, you can take a look.
I'm creating a list of collapsible using the react native flatlist component.
I'm using ref attribute to get the item clicked.
But when i try to access the ref from the click event, it doesn't take effect on the clicked item but on the last item in the flatlist.
export default class Update extends Component {
renderItems (data, index) {
return (
<TouchableNativeFeedback
onPress={() => this.expandView()}
>
<View style={[styles.itemWrapperExpandable]}>
<View style={styles.itemHeader}>
<View style={styles.itemAvatar}>
<Image source={require('../images/logo.png')} style={styles.avatar}></Image>
</View>
<View style={styles.itemContent}>
<Text style={[styles.itemTitle, styles.black]}>{data.name}</Text>
<Text style={[styles.rating, styles.grey]}>
{data.rating}<Icon name="star"></Icon>
</Text>
<Text style={[styles.content, styles.black]}>{data.description}</Text>
</View>
<View style={styles.itemBtn}>
<Icon name="chevron-down" style={{ color: '#000', fontSize: 22 }}></Icon>
</View>
</View>
<View ref={(e) => this._expandableView = e } style={[styles.itemBody]}>
<Text style={styles.itemBodyText}>
some more information about this update will appear here
some more information about this update will appear here
</Text>
</View>
</View>
</TouchableNativeFeedback>
);
}
expandView () {
LayoutAnimation.easeInEaseOut();
if (this._expandableView !== null) {
if (!this.state.isExpanded) {
// alert(this.state.isExpanded)
this._expandableView.setNativeProps({
style: {height: null, paddingTop: 15, paddingBottom: 15,}
})
}
else {
this._expandableView.setNativeProps({
style: {height: 0, paddingTop: 0, paddingBottom: 0,}
});
}
this._expandableView.setState(prevState => ({
isExpanded: !prevState
}));
}
}
render() {
return (
<FlatList
data={this.state.data}
renderItem={({ item, index }) => this.renderItems(item, index)}
/>
)
}
}
I also tried placing using the index of the items, but couldn't make it work.
Any way around this? I think the ref are overwritten by the next one when the items are rendering.
You are right about your assumption. Ref is overwriting with the next item so ref is the last item's ref. You can use something like below to set each items ref separately.
ref={(ref) => this.refs[data.id] = ref}
Of course this solution assumes you have a unique id or sort in your item data.
To paraphrase the React Native docs, direct manipulation (i.e. refs) should be used sparingly; unless you need it for some other reason I'm unaware of, refs aren't necessary in this case. Typically, the best way to keep track of selected items in a FlatList is by utilizing the keyExtractor and extraData props in conjunction with a Javascript Map object in state.
The way React is able to keep track of items being added/removed/modified is by using a unique key prop for each item (preferably an id, or if necessary indexes work if the list order will not change). In a FlatList, this is handled "automagically" if you will using the keyExtractor prop. To keep track of the selected item, we can add/remove items from our Map object whenever we click on one. Map is a type of object like an array that holds key-value pairs. We'll use this in state to store a key item.id and a boolean value true for each item that is selected.
So, we end up with something like this:
export default class Update extends Component {
state = {
data: [],
selected: (new Map(): Map<string, boolean>)
}
renderItems = ({ item }) => {
// note: the double ! operator is to make sure non boolean values are properly converted to boolean
return (
<ExpandableItem
item={item}
selected={!!this.state.selected.get(item.id)}
expandView={() => this.expandView(item)}
/>
);
}
expandView (item) {
LayoutAnimation.easeInEaseOut();
this.setState((state) => {
const selected = new Map(state.selected);
selected.set(item.id, !selected.get(item.id));
return {selected};
});
// the above allows for multiple expanded items at a time; the following will simultaneously close the last item when expanding a new one
// this.setState((state) => {
// const selected = new Map();
// selected.set(item.id, true);
// return {selected};
// });
}
render() {
return (
<FlatList
data={this.state.data}
keyExtractor={(item, index) => `${item.id}`}
renderItem={this.renderItems}
/>
);
}
}
const ExpandableItem = ({ item, selected, expandView }) => {
return (
<TouchableNativeFeedback onPress={expandView}>
<View style={styles.itemWrapperExpandable}>
{/* ...insert other header code */}
<View style={[styles.itemBody, selected && styles.itemBodySelected]}>
<Text style={styles.itemBodyText}>
some more information about this update will appear here
</Text>
</View>
</View>
</TouchableNativeFeedback>
);
}
You'll have to play around with the styles.itemBodySelected to make it look how you want. Note that the separate functional component <ExpandableItem /> for the renderItem isn't required, just how I prefer to structure my code.
Helpful links:
https://facebook.github.io/react-native/docs/flatlist.html
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
https://reactjs.org/docs/lists-and-keys.html#keys