Multi-Select Component: React Native
Tags: reactnative, mobile, typescript, tailwindcss
Published: 2024-03-01 14:22:30

I started learning React Native a year ago to build a mobile app for a company I started with my friends, I wasn't a mobile developer at the time but lack of resources meant I had to be. Learning how it worked was fairly easy thanks to my background knowledge of [React](https://react.dev/) and [Expo CLI](https://expo.dev/). Working with the two meant picking up mobile development was somewhat easy, some what but not completely easy.

## The Problem
React Native comes with a lot of helpful components that seem to have been built on HTML tags, so if you're familiar with HTML then you're halfway through becoming a React Native developer. One component in particular of interest is the `<select></select>` tag in HTML that doesn't seem to be present in the framework, if you're unfamiliar with how it works, its a little something like this:

```html
<select id="cars" name="cars" multiple>
  <option value="volvo">Volvo</option>
  <option value="toyota">Toyota</option>
  <option value="mercedes">Mercedes</option>
  <option value="audi">Audi</option>
</select>
```

> Note: I added the multiple so in order to select multiple options

This tag doesn't have a counterpart in React Native and if you're like me and don't like being dependent on people or packages then you would want something that came packaged with the framework, rather than having to install 3rd party libraries. Of course if you do, there's no judgement here and  there are some pretty good options you could try like [Nativebase](https://docs.nativebase.io/select). So now we don't want 3rd party libraries and React Native doesn't have a <Select /> component, what do we do? we're developers, we make ours.

## The Solution?
Thankfully React Native comes with a very useful component called FlatList, it's a component used to efficiently render a list of data e.g a list of users or clubs. It is recommended if the data size is large, where rendering all items at once might lead to performance issues. It only renders the items that are visible on the screen. Here's an example of how its used:

```javascript
import { FlatList, Text, View } from 'react-native';

const data = [
  { id: '1', title: 'Volvo' },
  { id: '2', title: 'Toyota' },
  { id: '3', title: 'Mercedes' },
  { id: '4', title: 'Audi' },
  // Add more items as needed
];

const renderItem = ({ item }) => (
  <View style={{ padding: 10 }}>
    <Text>{item.title}</Text>
  </View>
);

const MyCarList = () => {
  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={item => item.id}
    />
  );
};

export default MyCarList;
```
It is a very powerful component I prefer using over the ScrollView to render a list of items. So, how does this almighty component help us select multiple items? It's fairly simple really, we create our own component that uses the FlatList to display our data.

## MultiSelect Component
We're going to need one more component from React Native and that is the TouchableOpacity (when we select an item we want some feedback). The first thing we do is get our imports:

```javascript
import React from 'react';
import { FlatList, Text, TouchableOpacity, View } from 'react-native';
```

Then we define our interface for this new component we are creating.

```javascript
export type TouchableOpacityProps = DefaultTouchableOpacity["props"];

interface MultiSelectProps<T> extends TouchableOpacityProps {
  items: (string | T)[];
  valueKey?: keyof T;
  displayKey?: keyof T;
  selectedItems: string[];
  setSelectedItems: React.Dispatch<React.SetStateAction<string[]>>;
}
```
This extends the TouchableOpacity Props, this way we get the props associated with Touchable Opacity, at this point you're wondering why TouchableOpacity and not FlatList, it'll be clear as we go along.

Lets breakdown the MultiSelectProps:

- `MultiSelectProps<T>`- this is the name of the prop but using typescript, we can set a generic for the type of data our FlatList would be working with.
- `items: (string | T)[]` - This is a list of the items we will be displaying, as you can see it can either be a string or the T we defined as the generic
-  `valueKey?: keyof T` - This is the value we want when select an item e.g `ID` and it has to be a Key that exists on the T type
-`displayKey?: Keyof T` - Similar to the valueKey, this is used to display content the user e.g `name`
- `selectedItems: string[]` - All the items the user selects will be returned to this callBack function
- `setSelectedItems` - In order to know what Items were selected we also need the state

Next we have our MultiSelect definition:

```javascript
export function MultiSelect<T>(props: MultiSelectProps<T>): JSX.Element {
  const {
    items,
    valueKey,
    displayKey,
    selectedItems,
    setSelectedItems,
    ...otherprops
  } = props;
```

We need a way to handle the selected items the user clicks on:

```javascript
  const handleSelect = (item: string | T) => {
    setSelectedItems((prev) => {
      const itemId = valueKey ? (item as T)[valueKey] : item;
      if (prev.includes(String(itemId))) {
        return prev.filter((i) => String(i) !== String(itemId));
      }
      return [...prev, String(itemId)];
    });
  };
```
The `handleSelect` function updates the selected items list by toggling the selection state of the provided item based on its ID or valueKey.

Finally we get to the part where we render the options given to us with the items:

```javascript
  return (
    <FlatList
      data={items}
      keyExtractor={(item, index) =>
        valueKey ? String((item as T)[valueKey]) : String(index)
      }
      renderItem={({ item }) => (
        <TouchableOpacity
          onPress={() => handleSelect(item)}
          className="flex flex-row items-center justify-between w-full h-10"
          {...otherprops}
        >
          <Text>{displayKey ? (item as T)[displayKey] : item}</Text>
          <View
            className={`border border-black rounded-full w-4 h-4 ${
              selectedItems.includes(
                valueKey ? String((item as T)[valueKey]) : String(item),
              )
                ? "bg-black"
                : "bg-white"
            }`}
          />
        </TouchableOpacity>
      )}
    />
  );
}
```

Putting it all together we have:

```javascript
import React from 'react';
import { FlatList, Text, TouchableOpacity, View } from 'react-native';

interface MultiSelectProps<T> {
  items: (string | T)[];
  valueKey?: keyof T;
  displayKey?: keyof T;
  selectedItems: string[];
  setSelectedItems: React.Dispatch<React.SetStateAction<string[]>>;
}

export function MultiSelect<T>(props: MultiSelectProps<T>): JSX.Element {
  const {
    items,
    valueKey,
    displayKey,
    selectedItems,
    setSelectedItems,
    ...otherprops
  } = props;

  const handleSelect = (item: string | T) => {
    setSelectedItems((prev) => {
      const itemId = valueKey ? (item as T)[valueKey] : item;
      if (prev.includes(String(itemId))) {
        return prev.filter((i) => String(i) !== String(itemId));
      }
      return [...prev, String(itemId)];
    });
  };

  return (
    <FlatList
      data={items}
      keyExtractor={(item, index) =>
        valueKey ? String((item as T)[valueKey]) : String(index)
      }
      renderItem={({ item }) => (
        <TouchableOpacity
          onPress={() => handleSelect(item)}
          className="flex flex-row items-center justify-between w-full h-10"
          {...otherprops}
        >
          <Text>{displayKey ? (item as T)[displayKey] : item}</Text>
          <View
            className={`border border-black rounded-full w-4 h-4 ${
              selectedItems.includes(
                valueKey ? String((item as T)[valueKey]) : String(item),
              )
                ? "bg-black"
                : "bg-white"
            }`}
          />
        </TouchableOpacity>
      )}
    />
  );
}
```

The MultiSelect component provides a versatile solution for creating multi-select interfaces in React Native applications. By accepting an array of items, along with optional valueKey and displayKey props for handling complex data structures, it offers flexibility in rendering and managing selections. The component efficiently utilizes FlatList for optimal performance, ensuring smooth interactions even with large datasets. With its customizable rendering and selection handling, the MultiSelect component simplifies the implementation of multi-select functionality across various use cases in React Native applications. An example of how to use the `<MultiSelect />`:

```javascript
import React, { useState } from 'react';
import MultiSelect from './MultiSelect';

const App = () => {
  const [selectedItems, setSelectedItems] = useState<string[]>([]);
  
  // Example data
  const cars = [
    { id: 'volvo', name: 'Volvo' },
    { id: 'toyota', name: 'Toyota' },
    { id: 'mercedes', name: 'Mercedes' },
    { id: 'audi', name: 'Audi' },
    // Add more items as needed
  ];

  return (
    <MultiSelect
      items={cars}
      valueKey="id"
      displayKey="name"
      selectedItems={selectedItems}
      setSelectedItems={setSelectedItems}
      style={{ width: 200, maxHeight: 200 }}
    />
  );
};

export default App;
```

And here is how it looks on screen:

![render showing multi select render](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y5gw511c4ta3asu5t5bh.png)

Feel free to abstract it as much as you like, add or take things out for your various use cases, _ciao!_