Search code examples
react-nativeindexingflatlist

React Native TouchableOpacity onPress using index/key of the item in FlatList


We created a FlatList component and a Contact screen in our app. We want to add 'heart' icon to the near of the images in Contact screen. We added heart icon near to all of items. But, if we pressed one of these icons, it changes all of theirs colors to red, not only one of them. We want to change the color of clicked item.

Screenshot of our program:

enter image description here

This is our FlatList component:

import React, { Component } from 'react';
import { View, Text, SafeAreaView, StyleSheet, FlatList, Image, TouchableOpacity, 
TouchableWithoutFeedback, TextInput } from 'react-native';
import { Right, Icon } from 'native-base';
import data from '../../data';

export default class FlatListComponent extends Component {
  state = {
    text: '',
    contacts: data,
    like: false,
    color: 'white',
  }

  toggleLike=()=>{
    this.setState({
        like: !this.state.like
    })

    if(this.state.like){
      this.setState({
        color: 'red',
    })
    }else{
      this.setState({
      color: 'white',
   })
 }
 }

renderContactsItem = ({item, index}) => {

return (
  <View style={[styles.itemContainer]}>
    <Image
      style={styles.avatar}
      source={{ uri: item.image }} />
    <View style={styles.textContainer}>
      <Text style={[styles.name], {color: '#fafafa'}}>{item.first_name}</Text>
      <Text style={{ color: '#fafafa' }}>{item.last_name}</Text>
    </View>
    <Right style={{justifyContent: 'center'}}>
      <TouchableWithoutFeedback onPress={this.toggleLike}>
        {/*{this.like ? (
            <Icon name="heart" type='FontAwesome' style={{paddingRight: 10, fontSize: 30, color: 'red'}} />
          ) : 
          ( <Icon name="heart" type='FontAwesome' style={{paddingRight: 10, fontSize: 30, color: 'white'}} /> )
        }*/}
        <Icon name='heart' type='FontAwesome' size={32} style={{color: this.state.color === "white" ? 'white' :'red', paddingRight: 10 }}/>
        {/*<Icon name="heart" type='FontAwesome' style={{paddingRight: 10, fontSize: 30, color: this.state.color}} />*/}
      </TouchableWithoutFeedback>
    </Right>
  </View>
);
}

searchFilter = text => {
  const newData = data.filter(item => {
    const listItems = `${item.first_name.toLowerCase()}`
    return listItems.indexOf(text.toLowerCase()) > -1;
  });

  this.setState({
    contacts: newData,
  });
};

renderHeader = () => {
  const {text} = this.state;
  return (
    <View style={styles.searchContainer}>
      <TextInput 
        onChangeText = {text => {
          this.setState ({
            text,
          });
          this.searchFilter(text);
        }}
        value={text}
        placeholder="Search..." 
        style={styles.searchInput} />
    </View>
  )
}

render() {
  return (
    <FlatList
      ListHeaderComponent={this.renderHeader()}
      renderItem={this.renderContactsItem}
      keyExtractor={item => item.id}
      data={this.state.contacts}
    />
  );
}
}

 const styles = StyleSheet.create({
itemContainer: {
  flex: 1,
  flexDirection: 'row',
  paddingVertical: 10,
  borderBottomWidth: 1,
  borderBottomColor: '#eee'
},
avatar: {
  width: 50,
  height: 50,
  borderRadius: 25,
  marginHorizontal: 10,
},
textContainer: {
  justifyContent: 'space-around',
},
name: {
  fontSize: 16,
},
searchContainer: {
  padding: 10

},
searchInput: {
  fontSize: 16,
  backgroundColor: '#f9f9f9',
  padding: 10,
}
});

Our Contact screen is just:

import React from 'react';
import 'SafeAreaView' from 'react-native';
import FlatList from './FlatList';

export default function Contact() {
    <SafeAreaView style={{ flex: 1 }}>
        <FlatList />
    </SafeAreaView>
}

How can we implement this?


Solution

  • I've run into this recently :) One option is to make the renderContactsItem its own component. For example:

    const RenderContactsItem = ({item, index}) => {
    
     const [like, setLike] = useState(false);
     const [color, setColor] = useState("white");
    
     const toggleLike = () => {
      setLike(!like)
      if(like) {
       setColor("red");
      } else {
       setColor("white");
      }
     }
    
     return (
      <View style={[styles.itemContainer]}>
        <Image
          style={styles.avatar}
          source={{ uri: item.image }} />
        <View style={styles.textContainer}>
          <Text style={[styles.name], {color: '#fafafa'}}>{item.first_name}</Text>
          <Text style={{ color: '#fafafa' }}>{item.last_name}</Text>
        </View>
        <Right style={{justifyContent: 'center'}}>
          <TouchableWithoutFeedback onPress={toggleLike}>
            <Icon name='heart' type='FontAwesome' size={32} style={{color, paddingRight: 10 }}/>
          </TouchableWithoutFeedback>
    
    
        </Right>
       </View>
      );
    }
    

    In this case, each item manages its own state, so setting like doesn't set it for every item.

    Another option would be to build an object with "like" states and set the values as the hearts are pressed. For example:

    state = {
     text: '',
     contacts: data,
     like: {},
     color: 'white', // You don't need this
    }
    

    Then, when a heart is pressed, you can send toggleLike the index, and set the state like so:

    toggleLike = (index) => {
      let newLike = {...this.state.like};
      newLike[index] = !Boolean(newLike[index]);
      this.setState({
       like: newLike,
      });
    }
    

    And render the color conditionally depending on the index value of the like state, like so:

    <Icon name='heart' type='FontAwesome' size={32} style={{color: this.state.like[index] ? 'red' :'white', paddingRight: 10 }}/>