IMEOS
  1. Home
  2. Work
  3. Contact
  4. Blog

Im Obstgarten 7
8596 Scherzingen
Switzerland

+41 79 786 10 11
(CET/CEST, Mo-Fr, 09:00 - 18:00)
io@imeos.com

IMEOS on GitHub
React Native

React Native Select component with search field

May 10, 2022
2 minutes read
  • Usage
  • Reference
  • Code

Just a very flexible component for searching & selecting an item from a (potentially) long list of options.

There is no built in component in React Native, that properly enables selecting an item from a longer list. There are some 3rd-party solutions, but I found them either not really suitable for small mobile devices (for example producing hard to use dropdowns) or not flexible enough, so here is another take, including an (optional) live search.

Usage

import Select from 'components/Select'

<Select
  disabled={!isConnected}
  labelTextColor='black'
  name={'Select Country'}
  options={
    countries.map((
      country: {
        id: number,
        name: string,
        contractorName: string,
        flag: string
    }) => ({
      id: country.id,
      name: country.name,
      image: country.flag
    }))
  }
  onValueChange={(selectedId: number) => handleLanguageSwitching(selectedId)}
  selectedId={activeCountry.id}
  mainButtonStyle={{
    backgroundColor: 'transparent',
    borderColor: '#6c757d'
  }}
  mainButtonTextStyle={{color: 'black'}}
/>

Reference

Props

NameType
disabled?boolean
onValueChangeFunction
optionsoptions
selectedIdnumber
showSearch?boolean
labelTextColor?string
mainButtonTextStyle?TextStyle
mainButtonStyle?ViewStyle
namestring

Code

import React, { useState, useEffect } from 'react'
import { FlatList, Image, Input, Modal, Platform, Pressable, StyleSheet, Text, TextStyle, View, ViewStyle } from 'react-native'
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import { FontAwesome } from '@expo/vector-icons'

type item = {
  id: number,
  name: string
}

type options = [
  item
]

export default function Select(props: {
  disabled?: boolean,
  onValueChange: Function,
  options: options,
  selectedId: number,
  showSearch?: boolean,
  labelTextColor?: 'string',
  mainButtonTextStyle?: TextStyle,
  mainButtonStyle?: ViewStyle,
  name: string
}) {
  const [selectedItemId, setSelectedItemId] = useState(props.selectedId)
  const [selectedItemText, setSelectedItemText] = useState('')
  const [modalVisible, setModalVisible] = useState(false)
  const [search, setSearch] = useState('')
  const [filteredOptions, setFilteredOptions] = useState(props.options)
  const showSearch = (props?.options.length > 15 || props.showSearch) && props.showSearch !== false
  const insets = useSafeAreaInsets()

  const setItem = (item: {id: number, name: string}) => {
    setModalVisible(false)
    setSelectedItemId(item.id)
    setSelectedItemText(item.name)
    setSearch('')

    // Return id and name
    props.onValueChange(item.id, item.name)
  }

  // Handle selectedId changes from parent
  useEffect(() => {
    setSelectedItemId(props.selectedId)

    // If item is preselected from parent, e.g. selectedId is defined
    if(props.options.length > 0 && props.selectedId > 0) {
      const selectedItem = props.options.find(item => item.id === props.selectedId)
      if(selectedItem?.name) {
        setSelectedItemText(selectedItem.name)
      }
    }
  }, [props.selectedId])

  // Handle options changes from parent
  // Set up with options data
  useEffect(() => {
    if(props.options) {
      setFilteredOptions(props.options)
      if(!props.selectedId || props.selectedId === 0) {
        setSelectedItemText('Please select …')
      }
    }
    // setSelectedItemText, if only one option exists
    if(props.options.length === 1) {
      setItem(props.options?.[0])
    }
  }, [props.options])

  // Handle search
  useEffect(() => {
    // Check if searched text is not blank
    if (search.length > 0) {
      // Inserted text is not blank
      // Filter the options and update FilteredDataSource
      const newData = props.options.filter((item: {name: string}) => {
        // Applying filter for the inserted text in search bar
        const itemData = item.name
          ? item.name.toUpperCase()
          : ''.toUpperCase()
        const textData = search.toUpperCase()
        return itemData.indexOf(textData) > -1
      })
      setFilteredOptions(newData)
    } else {
      // Inserted text is blank
      // Update FilteredDataSource with props.options
      setFilteredOptions(props.options)
    }
  }, [search])

  // We need to call this Component as a function, otherwise the search field loses focus on every keystroke
  const FlatListHeader = () => {
    return (
      <>
        { showSearch &&
          <>
            <SafeAreaView>
              <FontAwesome name='search' size={17} style={styles.searchIcon} />
              <Input
                autoCorrect={false}
                containerStyle={{marginTop: 0}}
                onChangeText={(text: string) => setSearch(text)}
                placeholder='Suchen …'
                style={styles.searchInput}
                underlineColorAndroid='transparent'
                value={search}
              />
              {/* clear button */}
              { (search.length > 0) ? (
                <Pressable
                  onPress={() => setSearch('')}
                  style={styles.searchClearButton}
                >
                  <FontAwesome name='times-circle' size={17} color='#999' />
                </Pressable>
              ) : null
              }
            </SafeAreaView>
            <FlatListSeparator />
          </>
        }
      </>
    )
  }

  type Item = {
    id: number
    name: string
    image?: string
  }

  // Single list item
  const FlatListItem = ({ item }: { item: Item }) => {
    const isActive = selectedItemId === item.id

    return (
      <Pressable
        onPress={() => setItem(item)}
        style={
          ({ pressed }) => ({
            opacity: pressed ? 0.5 : 1,
            ...styles.item,
            ...{backgroundColor: isActive ? '#c9e9e8' : 'transparent'},
            ...{paddingRight: insets.right},
            ...{paddingLeft: insets.left}
          })
        }
      >
        <Text style={styles.itemText}>
          {item.name}
        </Text>
        { item.image &&
          <Image
            style={styles.itemImage}
            resizeMode={'contain'}
            source={{ uri: item.image }}
          />
        }
      </Pressable>
    )
  }

  const FlatListSeparator = () => {
    return (
      <View
        style={{
          height: StyleSheet.hairlineWidth,
          width: '100%',
          backgroundColor: '#999',
        }}
      />
    )
  }

  const LabelAndButton = () => {
    return (
      <View style={styles.labelAndButton}>
        <Text
          style={{fontWeight: 'bold', color: props.labelTextColor}}
        >
          {props.name}
        </Text>
        <Pressable
          disabled={props.disabled}
          onPress={() => { setModalVisible(true) }}
          style={{
            ...styles.mainButton,
            ...{backgroundColor: props.disabled ? '#dcdde2' : 'white'},
            ...props.mainButtonStyle
          }}
        >
          <Text
            numberOfLines={1}
            style={{
              flex: 1,
              ...{color: props.disabled ? '#888' : 'black'},
              ...props.mainButtonTextStyle
            }}
          >
            {selectedItemText?.replace(/(\r\n|\n|\r)/gm, ' – ')}
          </Text>
          <FontAwesome
            name='angle-down'
            size={17}
            style={{
              ...{color: props.disabled ? '#888' : 'black'},
              marginTop: 1,
              ...props.mainButtonTextStyle
            }}
          />
        </Pressable>
      </View>
    )
  }

  return (
    <>
      <Modal
        animationType='slide'
        hardwareAccelerated
        onRequestClose={() => setModalVisible(false)}
        presentationStyle='formSheet'
        statusBarTranslucent={true}
        supportedOrientations={['portrait', 'landscape']}
        transparent={false}
        visible={modalVisible}
      >
        <SafeAreaView style={styles.modalHeader}>
          <Pressable
            onPress={() => { setModalVisible(!modalVisible) }}
            style={{flex: 1}}
          >
            <Text style={styles.modalCloseText}>
              Fertig
            </Text>
          </Pressable>
          <Text style={{fontSize: 18, fontWeight: '800'}}>
            Bitte auswählen …
          </Text>
          {/* empty element needed for flex alignment */}
          <View style={{flex: 1}} />
        </SafeAreaView>
        <FlatList
          data={filteredOptions}
          keyExtractor={(item, index) => index.toString()}
          initialNumToRender={20}
          keyboardShouldPersistTaps='handled'
          ItemSeparatorComponent={FlatListSeparator}
          ListHeaderComponent={FlatListHeader()}
          ListFooterComponent={<View style={{height: insets.bottom}} />}
          renderItem={FlatListItem}
          style={{backgroundColor: '#f5f5f5', height: '100%'}}
        />
      </Modal>

      <LabelAndButton />
    </>
  )
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: 'white',
    flex: 1
  },
  labelAndButton: {
    marginTop: 15,
    marginLeft: 0,
    marginBottom: 7
  },
  mainButton: {
    justifyContent: 'space-between',
    flexDirection: 'row',
    fontFamily: 'Muli',
    fontSize: 15,
    textAlignVertical: 'top',
    marginTop: 8,
    paddingHorizontal: 10,
    paddingTop: 11,
    paddingBottom: 11,
    backgroundColor: 'white',
    borderColor: '#979baa',
    borderRadius: 8,
    borderWidth: 1
  },
  modalHeader: {
    flexDirection: 'row',
    paddingRight: 15,
    paddingVertical: 15,
    paddingLeft: 15,
    borderBottomColor: '#999',
    borderBottomWidth: StyleSheet.hairlineWidth,
  },
  modalCloseText: {
    color: '#27b6af',
    fontSize: 16,
    fontWeight: 'bold',
    marginTop: 2
  },
  searchIcon: {
    position: 'relative',
    top: Platform.OS === 'ios' ? 25 : 33,
    left: 25,
    zIndex: 1,
    width: 20,
    color: '#999',
  },
  searchInput: {
    marginTop: -18,
    marginBottom: 15,
    marginHorizontal: 15,
    paddingLeft: 30,
    backgroundColor: '#e3e4e8',
    borderRadius: 8,
    borderColor: 'transparent',
  },
  searchClearButton: {
    position: 'absolute',
    right: 15,
    top: Platform.OS === 'ios' ? 20 : 28,
    zIndex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    width: 30,
    height: 30,
  },
  item: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between'
  },
  itemImage: {
    width: 60,
    height: 30,
    marginRight: 15
  },
  itemText: {
    fontSize: 16,
    flex: 1,
    padding: 15,
  },
})

Related posts

  • React NativeExpoRailsHerokuDeploy Expo Router React Native Web app on Heroku
    Nov 19, 2022
    7 minutes read
  • RailsDeviseImplementing Passwordless Authentication with WebAuthn and Passkeys in Rails
    Nov 2, 2025
    7 minutes read
  • RailsAWSAmazon SES for transactional emails
    Mar 3, 2025
    15 minutes read

  • Privacy
  • Imprint
IMEOS