Isaac Oluwatemilorun
Isaac Oluwatemilorun

Reputation: 576

React Native ref in Flatlist items. Returning for last item alone

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.

Upvotes: 2

Views: 5640

Answers (2)

swimisbell
swimisbell

Reputation: 1581

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

Upvotes: 1

bennygenel
bennygenel

Reputation: 24660

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.

Upvotes: 3

Related Questions