【React-Select】便利なセレクトボックスライブラリで、選択肢や入力フィールドをカスタムする方法

## Overview
- react-selectで選択肢をカスタムするには `formatOptionLabel` 属性を使用する
- 「ドロップダウンメニューの選択肢」と「選択後の値」を出し分けるには `meta` 引数を使用する


## はじめに
[react-select](https://react-select.com/home) は、Reactでセレクトボックスを実装するためのライブラリです。
本記事では、ドロップダウンメニューの選択肢表示をカスタマイズする方法を紹介します。

![参考画像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3457770/8b8c830a-0202-4666-81c5-14149dd95149.jpeg) 

## 選択肢をカスタマイズする
```tsx:src/components/CustomSelect.tsx
import Select from 'react-select';

interface Option {
  role: 'admin' | 'none';
  name: string;
}

const options: Option[] = [
  { role: 'admin', name: 'John' },
  { role: 'none', name: 'Alice' },
];

const CustomSelect = () => {
  const FormatOptionLabel = (data: Option) => {
    const isAdmin = data.role === 'admin';
    return (
      <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
        <div style={{ color: isAdmin ? 'blue' : 'black', width: '60px' }}>{isAdmin ? '管理者' : '一般'}</div>
        <div style={{ color: 'black' }}>{data.name}</div>
      </div>
    );
  };

  return (
    <Select
      options={options}
      formatOptionLabel={(data) => FormatOptionLabel(data)}
      getOptionValue={(option) => option.name} // 識別子
    />
  );
};

export default CustomSelect;

```

### 選択肢の型 & 選択肢を定義する
任意の型構造で大丈夫です。
```tsx
interface Option {
  role: 'admin' | 'none';
  name: string;
}

const options: Option[] = [
  { role: 'admin', name: 'John' },
  { role: 'none', name: 'Alice' },
];
```

### `FormatOptionLabel` 関数を作成する
今回は、管理者ユーザーのみ色を変更するようにしました。

詳しくは後述しますが、全ての`key`を使用する必要はありません。
```tsx
const FormatOptionLabel = (data: Option) => {
    const isAdmin = data.role === 'admin';
    return (
      <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
        <div style={{ color: isAdmin ? 'blue' : 'black', width: '60px' }}>{isAdmin ? '管理者' : '一般'}</div>
        <div style={{ color: 'black' }}>{data.name}</div>
      </div>
    );
};
```

### `Select` にプロパティを渡す
```tsx
return (
    <Select
      options={options}
      formatOptionLabel={(data) => FormatOptionLabel(data)}
      getOptionValue={(option) => option.name} // 識別子
    />
)
```
:::note
### `getOptionValue`プロパティ

デフォルトでは「選択肢の`value`プロパティ」によって「どの選択肢が選ばれているか」が識別されます。
今回は選択肢が`value`プロパティを持たないため、代わりに`name`を使用するよう設定しています。
| 成功例 | `getOptionValue`がない場合 |
|:-:|:-:|
| ![成功例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3457770/4f4670f1-b996-4613-bf5f-0ce0fddd92bf.jpeg) | ![失敗例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3457770/ce9ab75e-03bf-474e-81ae-80596dcc27ff.jpeg)  |
| 選択されたもののみ強調されている  | 全て強調されてしまう  |
:::


## 選択後の表示をカスタマイズする
```tsx
import Select, { FormatOptionLabelMeta, StylesConfig } from 'react-select';

interface Option {
  role: 'admin' | 'none';
  name: string;
}

const options: Option[] = [
  { role: 'admin', name: 'John' },
  { role: 'none', name: 'Alice' },
];

const customStyles: StylesConfig<Option, false> = {
  control: (provided) => ({
    ...provided,
    width: '200px', // コントロール（選択エリア）の幅を設定
  }),
};

const CustomSelect = () => {
  const FormatOptionLabel = (data: Option, meta: FormatOptionLabelMeta<Option>) => {
    const isAdmin = data.role === 'admin';
    return meta.context === 'menu' ? (
      <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
        <div style={{ color: isAdmin ? 'blue' : 'black', width: '60px' }}>{isAdmin ? '管理者' : '一般'}</div>
        <div style={{ color: 'black' }}>{data.name}</div>
      </div>
    ) : (
      <div style={{ color: 'black' }}>{data.name}</div>
    );
  };

  return <Select options={options} formatOptionLabel={FormatOptionLabel} getOptionValue={(option) => option.name} styles={customStyles} />;
};

export default CustomSelect;
```

### `meta` を使用し、表示の出し分けを行う
先ほどとの違いは`meta`を使用し、表示を出し分けている点です。
`meta`は`'menu'`と`'value'`の2つの値を持ちます。
`'menu'`はドロップダウンメニューに対して、
`'value'`は選択後の表示に対して発火します。
| 1 | 2 |
|:-:|:-:|
| ![スクリーンショット 2025-02-17 19.06.39.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3457770/5d1f3e1e-4fab-4517-9895-3d1ff3b2d089.jpeg)  |  ![スクリーンショット 2025-02-17 19.07.14.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3457770/9a787519-fe01-41b8-968b-fcacff49f9e2.jpeg)|
|  選択肢では`role`が表示されている | 選択後は名前のみ表示  |

```tsx
return meta.context === 'menu' ? (
      <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
        <div style={{ color: isAdmin ? 'blue' : 'black', width: '60px' }}>{isAdmin ? '管理者' : '一般'}</div>
        <div style={{ color: 'black' }}>{data.name}</div>
      </div>
    ) : (
      <div style={{ color: 'black' }}>{data.name}</div>
);
```



### `customStyle`を適用
`menu` / `value` で表示を出し分けると、以下画像1のようなレイアウト崩れが発生します。
`customStyle`を適用することで、柔軟なレイアウトを実現可能です。
| 1 | 2 |
|:-:|:-:|
| ![スクリーンショット 2025-02-17 19.06.15.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3457770/7110918d-5d33-4261-97e0-c4474d41e102.jpeg)  |  ![スクリーンショット 2025-02-17 19.12.07.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3457770/8b8c830a-0202-4666-81c5-14149dd95149.jpeg) |
|  `menu`のレイアウト崩れ |  コンパクトなセレクトボックス |
```tsx
const customStyles: StylesConfig<Option, false> = {
  control: (provided) => ({
    ...provided,
    width: '200px', // コントロール（選択エリア）の幅を設定
  }),
};

// ~~

return <Select 
  options={options} 
  formatOptionLabel={FormatOptionLabel} 
  getOptionValue={(option) => option.name} 
  styles={customStyles} />;

```

`menu`も設定する場合
```tsx
const customStyles: StylesConfig<Option, false> = {
  menu: (provided) => ({
    ...provided,
    width: '200px', // メニューの幅を設定
  }),
  control: (provided) => ({
    ...provided,
    width: '100px', // コントロール（選択エリア）の幅を設定
  }),
};
```

## おわりに
`react-select`は（公式の情報が弱い節はありますが）とても便利なライブラリです。

ぜひ使ってみてください！