概要
おはこんばんにちわ。お疲れ様です。
React
でマルチセレクト(複数選択)のセレクトボックスを作成したときに
選択できない問題にぶち当たってなんとか解決したので
備忘録ついでにそっと置いておこうと思います。
環境
- React 18.2.0
始めに
※コーラを入れたいけど選べない様子を表してます
選択できない...エラーとか何も出てないのに...なんで?
みたいなことに対してアプローチしていきます。
※背景のピンクは選択状態をわかりやすくするためにcssでつけてます。
結論
useRefで解決できました。
以下、参考にどうぞです。
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
属性が付与されて値が無効になっている(クリックしても何も起こらない)ことを表しています。
これが曲者で操作によっては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
}
このコードによってセレクトボックス内の要素に対して、選択状態をリセットしています
しかし、disabledOptionによってfruitListに格納されている値はdisabled状態になるということです。
最後に
まあまあコアなパターンだとは思いますが、"セレクトボックス 選択できない"問題に関して書き垂らしてみました。
操作でちょっとひっかかると
えっ?ってなりますよね。
なんかの役に立つことを願って、また次の記事で会いましょう
※あんことコーラを一緒にすることを否定しているわけではありません
※自分は嫌だなと思っただけです