2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-12-22

概要

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

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

環境

  • React 18.2.0

始めに

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

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

※背景のピンクは選択状態をわかりやすくするために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("<おk")
  }

  // 選択されている値は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

修正後

解説

なぜ選択できないのか

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

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

スクリーンショット 2023-12-20 223414.png

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

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

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

useRef

公式ページを雑に置いておきます。

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

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

具体的に

まずはこちらをご覧ください。

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

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

次に以下のようにコードを修正して試してみます。

// コメントアウト
// const disabledOption = (key) => {
//   return fruitList.includes(key)
// }
    <option
      key={key}
      value={key}
      // disabled={disabledOption(key)}  // コメントアウト
      onClick={() => {console.log('クリック!')}}
    >

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

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

なぜ

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

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

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

値をクリック

handleSelectedFruitが発火し、fruitListが更新される

fruitListが変化したので、disabledOptionが発火する。

optionタグにdisabled={disabledOption(key)}とあるので trueの値はdisabledが付与される

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

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

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

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

selectedIndex プロパティ

このコードによってセレクトボックス内の要素に対して、選択状態をリセットしています
しかし、disabledOptionによってfruitListに格納されている値はdisabled状態になるということです。

最後に

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

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

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

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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?