マルチセレクトボックスがいうこと聞かない(React)

# 概要
おはこんばんにちわ。お疲れ様です。

`React`でマルチセレクト(複数選択)の**セレクトボックス**を作成したときに
選択できない問題にぶち当たってなんとか解決したので
備忘録ついでにそっと置いておこうと思います。

# 環境
- React 18.2.0

# 始めに
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3193405/5e0d8fa2-8608-be1a-80dd-04f256e88d72.gif" height=500 width=500>

※コーラを入れたいけど選べない様子を表してます

選択できない...エラーとか何も出てないのに...なんで？
みたいなことに対してアプローチしていきます。

※背景のピンクは選択状態をわかりやすくするためにcssでつけてます。

# 結論
useRefで解決できました。

以下、参考にどうぞです。

```React.jsx
import React, { useState, useRef } from 'react'

// 具材リスト
const selectFruits = [
  "漢は黙って汁のみ", "リンゴ", "バナナ", "メロン", "スイカ",
  "だんご", "パイナップル", "もも", "キウイフルーツ",
  "あんこ", "コーラ",
]

const Select = () => {
  const [fruitList, setFruitList] = useState(["漢は黙って汁のみ"])
  const [error, setError] = useState()
  const selectRef = useRef(null)

  // state更新
  const handleSelectedFruit = (e) => {
    const selects = Array.from(e.target.selectedOptions, (option) => option.value)

    // あんことコーラは一緒にしたくないよね？
    if (
      ((selectValues.includes("あんこ") && selectValues.includes("コーラ"))) ||
      ((fruitList.includes("あんこ") && selectValues.includes("コーラ"))) ||
      ((selectValues.includes("あんこ") && fruitList.includes("コーラ")))
    ) {
      setError("＜そりゃあないぜ")
      return
    }

    for (const value of selectValues) {
      // "汁のみ"と他は共存させない
      if (value === "漢は黙って汁のみ") {
        setFruitList(["漢は黙って汁のみ"])
        setError("＜なぬ")
        return
      }
    }

    let newSelectedValues
    if (fruitList[0] === "漢は黙って汁のみ") {
      // fruitListが"汁のみ"の場合はselectValuesのみを設定
      newSelectedValues = selectValues
    } else {
      // それ以外の場合はfruitListとselectValuesを結合
      newSelectedValues = [...new Set([...fruitList, ...selectValues])]
    }
    setFruitList(newSelectedValues)
    setError("＜おｋ")
  }

  // 選択されている値はdisable属性を付与
  const disabledOption = (value) => {
    return fruitList.includes(value)
  }

  // 削除
  const deleteFruit = (fruit) => {
    setError()
    const newfruitList = fruitList.filter((value) => value !== fruit)
    if (newfruitList.length === 0) {
      setFruitList(["漢は黙って汁のみ"])
    } else {
      setFruitList(newfruitList)
    }
    if (selectRef.current) {
      // 選択状態解除
      selectRef.current.selectedIndex = -1
    }
  }

  return (
    <>
      <div>
        <h3>フルーツポンチに何入れたい？</h3>
        <select
          size= "11"
          multiple={true}
          onChange = {handleSelectedFruit}
          ref={selectRef}
        >
          {selectFruits.map((value) => {
            return (
              <option
                key={value}
                value={value}
                disabled={disabledOption(value)}
              >
                {value}
              </option>
            )
          })}
        </select>
        <span onClick={() => setError('＜OMG!')}>( ﾟДﾟ)</span>
        <span>{error}</span>
      </div>

      <div>
        <p><strong>内容物</strong></p>
        {fruitList.map(value => (
          <div key={value}>
            {value}
            {value != "漢は黙って汁のみ" && (
              <div onClick={() => {deleteFruit(value)}}>✕</div>
            )}
          </div>
        ))}
      </div>
    </>
  )
}

export default Select
```

修正後

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3193405/7326100f-747c-e7e3-f8ef-ca1254f2887c.gif" height=500 width=500>


# 解説
## なぜ選択できないのか
セレクトボックスで選択されている値は、グレーの背景がつくと思います。

これはセレクトボックス内のoptionタグに`disabled`属性が付与されて値が無効になっている（クリックしても何も起こらない）ことを表しています。

![スクリーンショット 2023-12-20 223414.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3193405/45804e5a-e1d4-b950-9cf9-f9410595cbdd.png)

これが曲者で操作によってはdisabled属性が付与されたままになってしまいます。

一応他の部分をクリックしたりすると解除されるのですが
操作の手間が増えて面倒です。

そこで役に立ったのがReact Hooksの**useRef**でした。

## useRef

[**<font color=blue>公式ページ</font>**](https://ja.react.dev/reference/react/useRef)を雑に置いておきます。

簡単にいうと、Reactではstateが更新されたときにレンダリングが走ります。
そしてDOMが更新されて表示が変わるのですが、
useRefで指定した場所はレンダリングされず、変更を加えない限り初期値を保持し続けます。

```react
// 初期値null
const selectRef = useRef(null)
```
```react
<select
 size= "11"
 multiple={true}
 onChange = {handleSelectedFruit}
 // 指定
 ref={selectRef}
>
```

## 具体的に
まずはこちらをご覧ください。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3193405/4ab3f7f6-2da7-7560-505e-cd29b6a7b1b0.gif" height=300 width=300>

選ばれている値はセレクトボックス内で背景がついています。(disabled状態)

そこにクリックを試みていますが、( ﾟДﾟ)のコメントが変わらないです。


次に以下のようにコードを修正して試してみます。
```react
// コメントアウト
// const disabledOption = (key) => {
//   return fruitList.includes(key)
// }
```
```react
    <option
      key={key}
      value={key}
      // disabled={disabledOption(key)}  // コメントアウト
      onClick={() => {console.log('クリック！')}}
    >
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3193405/f88d453c-6afa-fa01-4ab5-26bd233bd4cd.gif" height=300 width=300>

先程とは違い、背景色がつかなくなりました。
また、すでに選択されている値をクリックすると
handleSelectedFruitが発火して( ﾟДﾟ)のコメントが変わりました。

背景色がついていない == disabled状態ではない(**無効化されていない！**)

**なぜ**

セレクトボックスは、selectFruitsの配列が変化していない為レンダリングの対象から外れて初期状態のままだからです。

('Д')＜さっきも変わってないじゃん

先程も確かにselectFruitsの配列の中身は変化していないですが、間接的にレンダリングの対象になっていました。
disabledOptionによって。

`値をクリック`
↓
`handleSelectedFruitが発火し、fruitListが更新される`
↓
`fruitListが変化したので、disabledOptionが発火する。`
↓
`optionタグにdisabled={disabledOption(key)}とあるので
trueの値はdisabledが付与される`

よって、レンダリングされて状態が変化したという流れになります。

値を**削除**するときも同様です。

ただ、削除のときはもうひと手間が必要です。
それがdeleteFruit内の以下に示すところになります。

```react
if (selectRef.current) {
  selectRef.current.selectedIndex = -1
}
 ```

 [selectedIndex プロパティ](https://developer.mozilla.org/ja/docs/Web/API/HTMLSelectElement/selectedIndex)
 
 このコードによってセレクトボックス内の要素に対して、選択状態をリセットしています
 しかし、disabledOptionによってfruitListに格納されている値はdisabled状態になるということです。


# 最後に
まあまあコアなパターンだとは思いますが、"セレクトボックス　選択できない"問題に関して書き垂らしてみました。

操作でちょっとひっかかると
えっ？ってなりますよね。

なんかの役に立つことを願って、また次の記事で会いましょう

※あんことコーラを一緒にすることを否定しているわけではありません
※自分は嫌だなと思っただけです