@rempei

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

<Select>①の値を変えたら<Select>②も対応した値に変える方法

解決したいこと

左が番号で右が名前を選ぶSelectコンポーネントだとします。(Selectコンポーネントはantdのもの)
例えば、左が二番が選ばれると右には二番のID を持つ人の名前が自動で入るようにするにはどのようにすればよいでしょうか?
どなたか分かる方がいらっしゃいましたら教えてほしいです。

image.png

該当するソースコード

import { Select } from "antd"
import { connect } from "react-redux"
import { useState } from "react"

const { Option } = Select

function TantoInput({ tanto }) {
  
  return (
    <>
      <Select 
        style={{width: 60}}>
        {tanto.map((tanto) => (
          <Option key={tanto.id} value={tanto.id}></Option>
        ))}
      </Select>
      <Select style={{width: 150}}>
        {tanto.map((tanto) => (
          <Option key={tanto.id} value={tanto.name}></Option>
        ))}
      </Select>
    </>
  )
}

const mapStateToProps = (state) => {
  return {tanto: state.tanto}
}

export default connect(mapStateToProps)(TantoInput)
//store.js

tanto: [
    { id: 1, name: "田中", },
    { id: 2, name: "佐藤", },
    { id: 3, name: "鈴木", },
  ]
1 likes

1Answer

ご質問のソースコードだといわゆる「非制御コンポーネント」と呼ばれる使い方ですので、stateを使って制御するのが良いと思います。
antdではありませんが、普通のselectタグであればこんな感じでやればよいかなと思います。
 https://codepen.io/charon1212/pen/xxaerGG
上記の例では、select要素にvalue属性とonChange属性を指定しています。value属性でstateの値を参照するようにし、onChangeでstateの値を更新するようにします。
「2つのリストの選択状態を常に等しくしたい」ということであれば、同じstateを使うことで実現できます。
もし「左のselectを選択した時だけ右のselectも同じものを選択させたい」という場合、2つのstateを用意してそれぞれに割り当て、左側のselectのonChangeの時に左右両方のstateを更新すれば実現できます。

・制御コンポーネントの説明は、この辺にありますね。Hooksではなくクラスベースの記述なので、その読み替えが必要ですが…
 https://ja.reactjs.org/docs/forms.html#controlled-components

・selectタグの制御コンポーネント化は、ここに記載がありました。
 https://react.dev/reference/react-dom/components/select#controlling-a-select-box-with-a-state-variable

・非制御コンポーネントは、この辺に記載があります。Reactで入力要素を扱う場合は、事情がない限り基本的に推奨されないと思います。
 https://ja.reactjs.org/docs/uncontrolled-components.html

2Like

Comments

  1. @rempei

    Questioner

    例のコードとその他の関連情報ありがとうございます。
    とても参考になりました。

    ちなみに普通のselectではうまくいったのですがantdのSelectでは
    TypeError: Cannot read properties of undefined (reading 'value')
    のエラーが表示されるのですがなぜかわかりますか?
  2. antdのSelectのAPI仕様を見ましたが、onChangeが通常のHTML要素とは違うかもしれませんね。
    <https://ant.design/components/select#select-props>

    普通のselect要素であれば、onChangeに渡す関数の第1引数はそのChangeイベントを表すイベントオブジェクトが来るのですが、
    antdはそこから変更後のvalueだけを渡すようにラップしてるんですかね。
    onChangeの「Type」列にある
    `function(value, option:Option | Array<Option>)`
    という記述からの推測だけですが…
    まぁ、上記API仕様の上にあるexampleでも、handleChange関数を`(value:string) => void`で定義していたりするので、多分そうじゃないだろうか…


    ということであれば、
    ```
    onChange={(e) => setSelectId(e.target.value)}
    ```
    と書いていたところを、
    ```
    onChange={(value) => setSelectId(value)}
    ```
    とかに直せば動くのかなと思います。
    たぶん`e.target`がundefinedでエラーになっているんじゃなかろうか…
  3. @rempei

    Questioner

    なるほど、ありがとうございます。そこを踏まえて直してみました。
    e.targetのとこだけ直すのだとうまく動かなかったので、管理するstateを増やしたりしてみて、かなり完成に近づいたのですが、
    ```React
    function TantoInput({ tanto }) {
    const [tId, setTId] = useState("")
    const [tName, setTName] = useState("")

    const handleChangeTId = (value) => {
    setTId(value)
    setTName(tanto[value-1].name)
    }
    const handleChangeTName = (value) => {
    setTId(tanto.id)            ←この行の指定方法
    setTName(value)
    }

    return (
    <>
    <Select
    style={{width: 60}}
    value={tId?.toString()}
    dropdownMatchSelectWidth={false}
    onChange={(value) => handleChangeTId(value)}
    >
    <Option ></Option>
    {tanto.map((t) => (
    <Option value={t.id}>{t.id}</Option>
    ))}
    </Select>
    <Select
    style={{width: 150}}
    value={tName?.toString()}
    dropdownMatchSelectWidth={false}
    onChange={(value) => handleChangeTName(value)}
    >
    <Option ></Option>
    {tanto.map((t) => (
    <Option value={t.name}>{t.name}</Option>
    ))}
    </Select>
    </>
    )
    }

    const mapStateToProps = (state) => {
    return {tanto: state.tanto}
    }

    export default connect(mapStateToProps)(TantoInput)
    ```
    上記コードで指定した部分について
    右側Selectの名前を指定したときに自動で左側にも番号を入れるというロジックの選んだvalueに対応する数をsetTIdの引数に指定する方法が分かりません。
    逆の番号を指定して名前を自動で入れるほうはできました。
    どのようにすればよいでしょうか?
  4. ■いったん、質問に直接お答えしますと…

    ```
    const handleChangeTName = (value) => {
    setTId(tanto.id)            ←この行の指定方法
    setTName(value)
    }
    ```



    ```
    const handleChangeTName = (value) => {
    const t = tanto.find((v) => v.name === value);
    if (t === undefined) throw new Error('エラー:選択した担当者がリストに存在しません。');
    setTId(t.id);
    setTName(t.name);
    }
    ```

    こんな風にするとかなのかなぁと思います。
    if文のエラーはTypeScript意識で書いただけなのでなくてもまぁ動くとは思います。

    ■ご提示いただいたコードだと、それ以外にいろいろ気になってしまうなーという感じます。

    ①stateは少ないほうが良い
    一般的に、Reactの設計ではstateは少ないほうが良いです。
    2つのSelectが同じ値を取り続けるのであれば、最初に示したサンプルのように1つのstateで表現する方が望ましいといえば望ましいです。
    何らかの都合で2つのSelectが別の値になりうるのであれば、2つのstateに分けるべきですが…

    ➁同姓同名でバグりそう
    2つ目のSelectについて、`<Option>`タグのvalueで担当者の名前を使っていると、valueが重複しそうです。
    例えば、
      [{id: 1, name: "佐藤"}, {id: 2, name: "佐藤"}, {id: 3, name: "佐藤"}, ]
    みたいなデータだと、どれを選択しても多分1人目の佐藤さんが必ず選ばれそうです。選択肢のvalueがどれも"佐藤"なので…

    これを避けるには、両方の選択状態を持つstateをどちらもIDで持ち、表示だけnameとします。

    ```
    const [tId1, setTId1] = useState(""); // 1,2って名前つけましたが、本来はちゃんと命名することが理想です。
    const [tId2, setTId2] = useState("");

    // ~~~この辺は省略~~~

    // Selectはvalue属性以外を省略。
    <Select value={tId1}>
    <Option/>
    {tanto.map((t) => (
    <Option value={t.id}>{t.id}</Option>
    ))}
    </Select>
    <Select value={tId2}> // 選択状態のvalueはNameではなくIDにする。
    <Option/>
    {tanto.map((t) => (
    <Option value={t.id}>{t.name}</Option> // ←Optionのvalueはidにして、右側の表示する部分はnameで表示する。
    ))}
    </Select>

    ```

    ③<数値>と<文字列>がごちゃごちゃしてる
    この辺は私のサンプル実装が悪いっていうのもあるんですが…
    tantoのidは数値を指定しているので、数値型で押し通したいですね。

    ```
    const [tId, setTId] = useState("")
    ```
    という定義で、tIdを文字列としてしまっているので…
    一方、tanto[0].idは数値なので、その辺が微妙かも入れないですね。

    handleChangeTIdにある`setTName(tanto[value-1].name)`というのも結構危ない書き方で、valueは文字列なので
      "5"-1 => 4
    のような、-演算子の数値変換を暗黙に使っているのが怖いと感じますね…
  5. @rempei

    Questioner

    まさにfindのところが知りたかったです!ありがとうございます!

    ①antdのSelectを使うという都合があり、最初のサンプルの方法だとうまく動かせなかったのでstateを増やしましたが、stateが一つのやり方があるのでしょうか?

    ②おっしゃる通り同名のデータを作ってみたら左のSelectは小さい数のIDが選ばれてしまいました。提示いただいた方法とfindのところのv.nameをv.idに直すことでうまくいきました!

    ③は自分も恐る恐るいけるかなぁと思いながら書いてみてちゃんと動いてほっとしたという感じでした。今回は暗黙の型変換をうまく使えた!という考え方は危険でしょうか?
  6. ①元々のサンプルはstate1つなので…
     UIライブラリが変わって見た目が変わっても、状態は1つのままにできると思いますが…
     ライブラリのバグとか、事情があればしょうがないとは思います!

    ③TypeScriptとか使っていればエラーを教えてくれるのですが、
     生のJavaScriptだとその辺の型に注意しないとバグが出そうで怖い印象です。
     自分一人が開発する物であれば、まぁバグったときに意識するくらいで良いかもしれませんが…
  7. @rempei

    Questioner

    なるほど、いろいろ教えていただいてありがとうございました!

Your answer might help someone💌