Prerequisites
To follow the post, you will need a React Native application, which can be easily created using this command:
npx react-native init ContactsList
For this example we will be using one of the WithFrame’s pre-built contacts screen.
Installation
If you’re using yarn, use the following command:
yarn add react-native-contacts
For NPM users we have this command:
npm install react-native-contacts --save
Now let’s install Pods for iOS with the following command:
npx pod-install
On React Native 0.60+ the CLI autolink feature links the module while building the app.
Permissions
Once the installation is done, we have to add permission strings to AndroidManifest.xml and Info.plist as shown below:
<uses-permission android:name="android.permission.READ_CONTACTS" />
<key>NSContactsUsageDescription</key>
<string>ContactsList needs your permission to access your contacts</string>
Step 1: Load all contacts
In order to load all device contacts we have to use the getAll()
method on the Contacts module.
Because this action is asynchronous, we have to call it inside a useEffect
and write the received data into a state.
import Contacts from 'react-native-contacts';
const [contacts, setContacts] = React.useState<Contacts.Contact[] | null>(
null,
);
React.useEffect(() => {
if (Platform.OS === 'android') {
PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.READ_CONTACTS, {
title: 'Contacts',
message: 'ContactsList app would like to access your contacts.',
buttonPositive: 'Accept',
}).then(value => {
if (value === 'granted') {
Contacts.getAll().then(setContacts);
}
});
} else {
Contacts.getAll().then(setContacts);
}
}, []);
Step 2: Group all contacts alphabetically by family name
Lodash library provides an excellent helper function for grouping; however, in our case, we will use good old-fashioned JavaScript.
First, let’s run a reduce on the array of contacts and convert them into an object like this: { "A": [CONTACT, CONTACT], "B": [CONTACT], ... }
Then, with the help of Object.entries()
, we can convert it back into an array with the following structure: [{ letter: "A", items: [CONTACT, CONTACT] }]
and sort all of the items in alphabetic order.
For performance reasons, we will wrap this code in a React.useMemo
hook. It will now only run when the contacts
array is changed.
const sections = React.useMemo(() => {
if (!contacts) {
return null;
}
const sectionsMap = contacts.reduce<Record<string, Contacts.Contact[]>>(
(acc, contact) => {
const {familyName} = contact;
const [firstLetter] = familyName;
return Object.assign(acc, {
[firstLetter]: [...(acc[firstLetter] || []), contact],
});
},
{},
);
return Object.entries(sectionsMap)
.map(([letter, items]) => ({
letter,
items: items.sort((a, b) => a.familyName.localeCompare(b.familyName)),
}))
.sort((a, b) => a.letter.localeCompare(b.letter));
}, [contacts]);
Step 3: Display ActivityIndicator until the data is available
As mentioned before, Contacts.getAll()
action is asynchronous, which is why we will display an ActivityIndicator until the data is available.
if (!sections) {
return (
<View
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#fff',
}}>
<ActivityIndicator />
</View>
);
}
Step 4: Update WithFrame’s template to use Contact’s properties
In this template, we’re using properties like img
and name
, however, they don’t exist on the Contact object.
For our template to work with the Contact object, let’s add the following local properties:
name
— a combination ofgivenName
andfamilyName
img
— defined asthumbnailPath
phone
— use the first item in thephoneNumbers
array or-
if the array is empty.
{sections.map(({letter, items}) => (
<View style={styles.section} key={letter}>
<Text style={styles.sectionTitle}>{letter}</Text>
<View style={styles.sectionItems}>
{items.map(
(
{givenName, familyName, phoneNumbers, thumbnailPath},
index,
) => {
const name = `${givenName} ${familyName}`;
const phone = phoneNumbers.length
? phoneNumbers[0].number
: '-';
const img = thumbnailPath;
return (
<View key={index} style={styles.cardWrapper}>
<TouchableOpacity
onPress={() => {
// handle onPress
}}>
<View style={styles.card}>
{img ? (
<Image
alt=""
resizeMode="cover"
source={{uri: img}}
style={styles.cardImg}
/>
) : (
<View style={[styles.cardImg, styles.cardAvatar]}>
<Text style={styles.cardAvatarText}>
{name[0]}
</Text>
</View>
)}
<View style={styles.cardBody}>
<Text style={styles.cardTitle}>{name}</Text>
<Text style={styles.cardPhone}>{phone}</Text>
</View>
<View style={styles.cardAction}>
<FeatherIcon
color="#9ca3af"
name="chevron-right"
size={22}
/>
</View>
</View>
</TouchableOpacity>
</View>
);
},
)}
</View>
</View>
))}