How to create a searchable contact list in react native

August 2, 2022 (2y ago)

This is a quick guide on how to create a serachable flat list component in React Native. The list in question is the users contact list which lives on their device. We are going to use expo to access this list.

Installation

To get started we need to install the expo package Contacts.

expo install expo-contacts

Basic component setup

Now that the package is installed we need to do some inital setup. Let's start by creating a contacts component (Contacts.jsx).

// Contacts.jsx
import {
  FlatList,
  Keyboard,
  KeyboardAvoidingView,
  Pressable,
  ScrollView,
  StyleSheet,
  Text,
  TextInput,
  View,
} from "react-native";
import React from "react";

const Contacts = () => {
  return (
    <KeyboardAvoidingView
      style={styles.container}
      behavior={Platform.OS= "ios" ? "padding" : "height"}
    >
      <Pressable onPress={Keyboard.dismiss} style={styles.dismisser}>
        <View style={styles.topContainer}>
          <Text style={styles.heading}>Contacts</Text>
        </View>
        {/* we are going to put the search bar here */}
        <View>{/* we are going to render contacts here */}</View>
      </Pressable>
    </KeyboardAvoidingView>
  );
};

export default Contacts;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    justifyContent: "center",
    padding: 16,
  },
  dismisser: {
    flex: 1,
  },
  topContainer: {
    alignItems: "center",
    justifyContent: "center",
  },
  heading: {
    fontSize: 28,
    fontWeight: "700",
  },
});

There is a lot going on there, go through it line by line. However, at the moment it is largely just an empty screen with some styling.

Next up we want to get our the users contacts and render them as a list.

Getting the users contacts

Below we are going to use the expo contacts package to get access the users contacts, have a look at the code block. I'll explain what is happening below.

// Contacts.jsx
import * as Contacts from 'expo-contacts';

const Contacts = () => {
  const [error, setError] = useState(undefined);
  const [contacts, setContacts] = useState();

    useEffect(() => {
    (async () => {
      const { status } = await Contacts.requestPermissionsAsync();
      if (status === 'granted') {
        const { data } = await Contacts.getContactsAsync({
          fields: [
            Contacts.Fields.FirstName,
            Contacts.Fields.LastName,
            Contacts.Fields.PhoneNumbers,
          ],
        });

        if (data.length > 0) {
          setContacts(data);
          // setFilteredContacts(contacts);
        } else {
          setError('No contacts found');
        }
      } else {
        setError('Permission to access contacts is denied');
      }
    })();
  }, []);

  //... component unchanged for now

In the above we are first importing the Contacts package from expo, then we set the state for error and contacts.

We then set a useEffect hook, this hook is first going to request permission from the user to access their contacts. If the user gives permission we are going to get the contact fields we want. Next we set contacts to the data that is returned from the users contacts.

If the user doesn't give us permission to access their contacts we are going to set an error that they denied premission.

If they gave us permission, then we need to render the contacts data to the screen. Let's look at how to do that.

Rendering the contacts using a FlatList

//Contacts.jsx
const Contacts = () => {
  //...

const renderItem = ({ item }) => {
  if (item !== undefined) {
    if (item.phoneNumbers) {
      return (
        <View style={styles.contact}>
          <View style={styles.contactRight}>
            <View style={styles.placeholderAvatar}>
              <Text>
                {item.firstName ? item.firstName[0] : ''}
                {item.lastName ? item.lastName[0] : ''}
              </Text>
            </View>
            <View>
              <Text>
                {item.firstName && item.LastName
                  ? `${item.firstName} ${item.lastName}`
                  : `${item.firstName}`}
              </Text>

              <Text>{item.phoneNumbers[0].number}</Text>
            </View>
          </View>
        </View>
      );
    }
  }
};

  return (
    //...

    <View style={styles.contactsContainer}>
      <Text style={styles.subHeading}>Your Contacts</Text>
      <FlatList
          data={contact}
          renderItem={renderItem}
          keyExtractor={(item)=> item.id}
        />
    </View>
  )
};

const styles = StyleSheet.create({
  //...
  contact: {
      flexDirection: 'row',
      alignItems: 'center',
      marginVertical: spaces.small,
    },
    placeholderAvatar: {
      marginRight: 10,
      justifyContent: 'center',
      alignItems: 'center',
      borderRadius: 100,
      height: 42,
      width: 42,
      backgroundColor: colors.primaryLight,
    },
)}

Before I explain what is happening with the code, I'm going to assume you understand how a FlatList works in React Native also (if not check the docs).

Above the first thing we do is create a renderItem component this component takes our contacts data and renders it out in the FlatList. The important thing to notice here is that we check that the item exists and we then check that item has a phoneNumebr because if this is missing we will get an error. So for what ever contact feild you want to check you need to ensure you are checking if it exists.

Also within the renderItem we do conditional render checks to ensure a field exist. If you're contacts are like mine they are a mess of some people having first names, some not so we want to make sure the user doesn't see undefined on the screen.

You will noticed that we have also added a FlatList which we pass out contact data to and some styling to the contacts.

Now, we have everything we need to get started with making out contact list searchable.

Searching contacts with expo

To make our contacts searchable in react native we need to add a few things to our code.

//Contacts.jsx

const Contacts = () => {
  //...
  const [searchText, setSearchText] = useState();

  const searchFilteredData = searchText
    ? contacts.filter((x) =>
        x.firstName.toLowerCase().includes(searchText.toLowerCase())
      )
    : contacts;

  return (
    //...
    <TextInput
        placeholder="Search..."
        value={searchText}
        onChangeText={(text)=> setSearchText(text)}
        style={styles.input}
      />
    <TextInput
        placeholder="Search..."
        style={styles.searchBar}
        setValue={setSearchText}
        value={searchText}
      />
      <View style={styles.contactsContainer}>
        <Text style={styles.subHeading}>Your Contacts</Text>
        <FlatList
          data={searchFilteredData}
          renderItem={renderItem}
          keyExtractor={(item)=> item.id}
          contentContainerStyle={styles.contactRows}
        />
      </View>
  )
};


//...
const styles = StyleSheet.create({
  //...
  input: {
    padding: spaces.medium,
    borderRadius: borders.input,
    marginVertical: spaces.small,
    borderWidth: 1,
    borderColor: colors.grey700,
    backgroundColor: colors.grey800,
  },
});

//TODO: clean this up rushed this to get draft done The first thing we added was a variable searchFilteredData this variable checks if searchText exists, if it does we filter our contacts list first names by the search text values otherwise we set it to equal the inital contacts value.

We get this data from our new TextInput component.

Finally we replace our FlatList data with the searchFilteredData variable so that it will only render the values returned from it.