'In a columnar FlatList, selecting any item in a row returns the last one, but only on iOS

In my React Native app, I have a FlatList of states that the user can choose from to filter a list. On Android, this list works fine; but on iOS no matter what item I tap on, only the last item in that row will show the opacity change and be treated as selected. I have this set to 5 columns in my app, but even changing it to 3 will still make the last item in the row be selected no matter what actual item I tapped on.

I have confirmed via console logs that every item has a unique value and that the item being returned is always the last item in the row (so in this case it will be 5, 10, 15, 20, etc). And again, this is only affecting my iOS app, not the android one.

Starting from the smallest component and working backwards (to save space I'm omitting the styling portion, but let me know if you need that):

StatePickerItem.js

import React from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';

import AppText from './AppText';
import IconImage from './IconImage';

function StatePickerItem({ item, onPress }) {
  return (
    <View style={styles.container}>
      <TouchableOpacity onPress={onPress}>
        <IconImage name={item.shortName} size={70} />
        <AppText numberOfLines={1} style={styles.label}>{item.fullName}</AppText>
      </TouchableOpacity>
    </View>
  );
}

AppPicker.js

import React, { useState } from 'react';
import { FlatList, Modal, StyleSheet, TouchableWithoutFeedback, View } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons'

import AppButton from './AppButton';
import AppText from './AppText';
import defaultStyles from '../config/styles';
import Screen from './Screen';
import StatePickerItem from './StatePickerItem';

function AppPicker({ clearFilter, items, onSelectItem, numberOfColumns = 1, placeholder, selectedItem }) {
  const [modalVisible, setModalVisible] = useState(false);

  return (
    <>
      <TouchableWithoutFeedback onPress={() => setModalVisible(true)}>
        <View style={[styles.container]}>
          {selectedItem ? (
            <AppText style={styles.text}>{selectedItem.shortName}</AppText>
          ) : (
            <AppText style={styles.placeholder}>{placeholder}</AppText>
          )}
          <MaterialCommunityIcons
            name="chevron-down"
            size={20}
            color={defaultStyles.colors.medium}
          />
        </View>
      </TouchableWithoutFeedback>
      <Modal visible={modalVisible} animationType='slide'>
        <Screen style={styles.modalScreen} hasNoHeader >
          <View style={selectedItem ? {marginBottom: 150} : {marginBottom: 75}}>
            <View style={styles.closeButton}>
              <MaterialCommunityIcons
                name='close'
                onPress={() => setModalVisible(false)}
                size={35}
                style={styles.closeX}
              />
              <View style={styles.clearFilter}>
                {selectedItem &&
                  <AppButton
                    color="secondary"
                    onPress={() => {
                      setModalVisible(false);
                      clearFilter(true);
                    }}
                    title="Clear Filter"
                  />
                }
              </View>
            </View>
            <View style={styles.grid}>
              <FlatList
                data={items}
                keyExtractor={(item) => item.value.toString()}
                horizontal={false}
                numColumns={numberOfColumns}
                renderItem={({ item }) => (
                  <StatePickerItem
                    item={item}
                    onPress={() => {
                      setModalVisible(false);
                      onSelectItem(item);
                    }}
                  />
                )}
              />
            </View>
          </View>
        </Screen>
      </Modal>
    </>
  );
}

MemorialListScreen.js

import React, { useEffect, useState } from 'react';
import { FlatList, RefreshControl, StyleSheet, View } from 'react-native';

import apiClient from "../api/client";
import AppPicker from '../components/AppPicker';
import AppTextInput from '../components/AppTextInput';
import colors from '../config/colors';
import ListItem from '../components/ListItem';
import ListItemSeperator from '../components/ListItemSeperator';
import listOfStates from '../config/states';
import Screen from '../components/Screen';


function MemorialListScreen({ navigation }) {
  const [displayList, setDisplayList] = useState();
  const [filteredList, setFilteredList] = useState();
  const [masterList, setMasterList] = useState();
  const [search, setSearch] = useState('');
  const [onRefresh, setOnRefresh] = useState(false);
  const [stateFiltered, setStateFiltered] = useState();
  const [stateFilteredList, setStateFilteredList] = useState();

  useEffect(() => {
    fetchMemorialList();
  }, []);

  const fetchMemorialList = () => {
    apiClient.get('/memorial-list').then((response) => {
      setStateFilteredList(response.data);
      setDisplayList(response.data);
      setFilteredList(response.data);
      setMasterList(response.data);
    }).catch((error) => {
      console.log(error);
    })
  }

  const handleRefresh = () => {
    setDisplayList(null);
    setFilteredList(null);
    setMasterList(null);
    setSearch(null);
    setStateFiltered(null);
    setStateFilteredList(null);

    fetchMemorialList();
  }

  const handleStateFilter = (selectedState) => {
    if (selectedState) {
      setStateFiltered(selectedState);
      const newData = masterList.filter((item) => {
        return (item.State.toUpperCase().indexOf(selectedState.shortName.toUpperCase()) > -1 );
      });
      setStateFilteredList(newData);
      setDisplayList(newData);
      setSearch(null);
    }
  }

  const searchFilter = (text) => {
    if (text) {
      const newData = stateFilteredList.filter((item) => {
        return (item.CategoryName.toUpperCase().indexOf(text.toUpperCase()) > -1 ||
          item.Name.toUpperCase().indexOf(text.toUpperCase()) > -1 ||
          item.City.toUpperCase().indexOf(text.toUpperCase()) > -1 ||
          item.Code.toUpperCase().indexOf(text.toUpperCase()) > -1 );
      });
      setDisplayList(newData);
      setSearch(text);
    } else {
      if (stateFiltered) {
        setDisplayList(stateFilteredList);
      } else {
        setFilteredList(filteredList);
      }
      setSearch(text);
    }
  }

  return (
    <Screen style={styles.screen} hasNoHeader>
      <View style={styles.searchRow}>
        <AppPicker
          clearFilter={() => {
            handleRefresh();
          }}
          items={listOfStates}
          numberOfColumns={5}
          onSelectItem={(selectedState) => handleStateFilter(selectedState)}
          placeholder="All"
          selectedItem={stateFiltered}
          style={styles.statePicker}
        />
        <AppTextInput
          icon="magnify"
          value={search}
          placeholder="Search"
          onChangeText={(text) => searchFilter(text)}
          style={styles.searchBox}
        />
      </View>
      <FlatList
        data={displayList}
        ItemSeparatorComponent={ListItemSeperator}
        keyExtractor={memorial => memorial.id.toString()}
        refreshControl={
          <RefreshControl
            refreshing={onRefresh}
            onRefresh={handleRefresh}
          />
        }
        renderItem={({item}) =>
          <ListItem
            category={item.CategoryName}
            cityState={item.City +", " + item.State}
            code={item.Code}
            image={item.SampleImage}
            name={item.Name}
            onPress={() => navigation.navigate("MemorialDetailScreen", {id: item.id})}
          />}
      />
    </Screen>
  );
}

states.js

This is just a list of 50 objects that contain the info for each state. I have included two below for reference, but they are in alphabetical order by state name and the values increment by 1 in the same order.

export default [
  {
    icon: "car",
    fullName: "Alabama",
    shortName: "AL",
    value: 1,
  },
  {
    icon: "car",
    fullName: "Alaska",
    shortName: "AK",
    value: 2,
  },
];

sample response of setMasterList(response.data)

This was requested by Vicky in the comments. Below are two of the memorial objects, but the actual response has about 3,100 of them.

[
    {
        "id": 1,
        "Name": "Central Texas Veterans Memorial",
        "Code": "TX2",
        "Category": 1,
        "Region": "Texas",
        "Latitude": "31.67790",
        "Longitude": "-98.99166",
        "Address1": null,
        "Address2": null,
        "City": "Brownwood",
        "State": "TX",
        "SampleImage": "2022TX2.jpg",
        "Access": "24/7",
        "MultiImage": false,
        "Restrictions": 1,
        "RallyYear": 2021,
        "URL": null,
        "createdAt": "2021-08-08T13:45:00.000Z",
        "updatedAt": null
    },
    {
        "id": 3,
        "Name": "59-09641 UH-1A",
        "Code": "H238",
        "Category": 5,
        "Region": "Illinois",
        "Latitude": "39.816330",
        "Longitude": "-89.668886",
        "Address1": null,
        "Address2": null,
        "City": "Springfield",
        "State": "IL",
        "SampleImage": "H238.jpg",
        "Access": "24/7",
        "MultiImage": false,
        "Restrictions": 1,
        "RallyYear": 2021,
        "URL": null,
        "createdAt": "2021-08-08T13:59:00.000Z",
        "updatedAt": null
    },
]

Screenshot of the State Picker

As requested by Leanne, here is a screenshot of what the picker looks like. iOS State Picker



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source