Reactで連動するプルダウンメニューをつくりました【React/Next.js/Form/セレクト/TypeScript】

JavaScript

選択した項目によって、次の選択内容が連動するプルダウンメニューをReactで作成したのでメモ。

作成したサンプル

結論から書くと、こちらのGitHubのコードを参考にすればOKです。

GitHub - matsu0314/react-pull-down
Contribute to matsu0314/react-pull-down development by creating an account on GitHub.

それだけだと説明が不足しているので、もう少し詳しくメモします。
今回は、フレームワークを選択するプルダウンメニューを作成します。フレームワークは言語ごとに提供されているものが異なり、バージョンもフレームワークごとに異なります。

これらを連動するプルダウンメニューで絞って選択できるようにします。

連動するプルダウンメニュは、言語、フレームワーク、バージョンの3つです。

選択した言語によって、次のプルダウンメニューで表示されるフレームワークが変わります。
さらに選んだフレームワークによって選択できるバージョンも変わります。

読み込むデータ

プルダウンメニューで表示するデータを次のような配列で用意します。
今回はダミーデータとして次のようなデータを準備しました。
TypeScriptなので、型も一緒に準備します。

react-pull-down/src/app/dummyData.ts at main · matsu0314/react-pull-down
Contribute to matsu0314/react-pull-down development by creating an account on GitHub.
// フレームワーク型
interface Framework {
  name: string;
  label: string;
  versions: string[];
}
// 言語型
interface MethodData {
  language: string;
  label: string;
  frameworks: Framework[];
}

export const methodData: MethodData[] = [
  {
    language: "Language1",
    label: "言語1",
    frameworks: [
      {
        name: "Framework1",
        label: "言語1-フレームワーク1",
        versions: ["1.0", "1.1", "2.0"],
      },
      {
        name: "Framework2",
        label: "言語1-フレームワーク2",
        versions: ["2.0", "2.1"],
      },
    ],
  },
  {
    language: "Language2",
    label: "言語2",
    frameworks: [
      {
        name: "Framework3",
        label: "言語2-フレームワーク3",
        versions: ["3.0", "3.1"],
      },
    ],
  },
  {
    language: "Language3",
    label: "言語3",
    frameworks: [
      {
        name: "Framework4",
        label: "言語3-フレームワーク4",
        versions: ["4.0", "4.1"],
      },
    ],
  },
];

配列には、言語、フレームワーク、バージョン(配列)をセットにしたオブジェクトが、言語の数だけ格納されます。

言語、フレームワーク、バージョンをセットにしたオブジェクト

{
  language: "Language1", // 言語(value)
  label: "言語1", // 言語(label)
  frameworks: [ // フレームワーク(配列)
    {
      name: "Framework1", // フレームワーク(value)
      label: "言語1-フレームワーク1", // フレームワーク(label)
      versions: ["1.0", "1.1", "2.0"], // バージョン(配列)
    },
    {
      name: "Framework2", // フレームワーク(value)
      label: "言語1-フレームワーク2", // フレームワーク(label)
      versions: ["2.0", "2.1"], // バージョン(配列)
    },
  ],
},

プルダウンメニューはこのデータを元に生成します。

プルダウンメニューコンポーネント

シンプルなプルダウンメニューコンポーネントを作成します。
型もあるのでちょっとコードが長いです。

react-pull-down/src/app/components/PullDown/index.tsx at main · matsu0314/react-pull-down
Contribute to matsu0314/react-pull-down development by creating an account on GitHub.
import React from "react";

type PullDownType = JSX.IntrinsicElements["select"] & {
    id?: string;
    label?: string;
    error?: string;
  };

  type Options = {
    value: string;
    label: string;
  };
  
  type OptionsProps = {
    options: Options[];
  };
  
  type Props = PullDownType & OptionsProps;

export const PullDown = React.forwardRef<HTMLSelectElement, Props>(
  (
    {
      className,
      id,
      label,
      error,
      options,
      ...props
    },
    forwardedRef
  ) => {
    return (
      <>
        {label && (
          <label htmlFor={id} className="form-label">
            {label}
          </label>
        )}
        <select
          id={id}
          className={
            className
          }
          ref={forwardedRef}
          {...props}
        >
          {options.map((option) => (
            <option key={option.value} value={option.value}>
              {option.label}
            </option>
          ))}
        </select>
        {/* エラーのCSSクラス名は環境によって変更する */}
        {error && <p className="mt-1 text-xs text-red-400">{error}</p>}
      </>
    );
  }
);

PullDown.displayName = "PullDown";

コンポーネントのPropsで受け取るのは、主にメニューの項目となる「options」や「label」です。
あとは、エラーがあった時の「error」 です。「error」はReact Hook Formなどのフォームライブラリで値を渡すようにすれば良さそうです。

プルダウンメニューの値をuseStateで管理する

コンポーネントを作成したら、プルダウンメニューでページを作成します。

react-pull-down/src/app/page.tsx at main · matsu0314/react-pull-down
Contribute to matsu0314/react-pull-down development by creating an account on GitHub.

まず、プルダウンメニューの言語、フレームワーク、バージョンの値をそれぞれuseState()で管理します。次のように初期化します。

  const [selectedLanguage, setSelectedLanguage] = useState("");  // 言語
  const [selectedFramework, setSelectedFramework] = useState("");  // フレームワーク
  const [selectedVersion, setSelectedVersion] = useState("");  // バージョン

プルダンメニューが変更になった時のイベントを設定する

プルダウンメニューを変更した時に発火するイベントを設定します。
useState()には、選択したプルダウンメニューのvalue値を格納するようにします。

// 言語を変更した時、
  const handleLanguageChange = (value: string) => {
    setSelectedLanguage(value);
    setSelectedFramework("");
    setSelectedVersion("");
  };
  // フレームワークを変更した時
  const handleFrameworkChange = (value: string) => {
    setSelectedFramework(value);
    setSelectedVersion("");
  };
  // バージョンを変更した時
  const handleVersionChange = (value: string) => {
    setSelectedVersion(value);
  };

言語、フレームワーク、バージョンを選択した時の動きは次のようになります。

  • 言語を選択した時
    選択した言語をuseState()にセットして、フレームワークとバージョンを初期化
  • フレームワークを選択した時
    選択したフレームワークをuseState()にセットして、バージョンを初期化
  • バージョンを選択した時
    選択したバージョンをuseState()にセットする

データを使ってプルダウンメニューを生成する

useState()とイベントを設定したら、コンポーネントを使ってプルダンメニューを作成します!

まず、冒頭で作成したダミーデータを読み込みます。

import { methodData } from "./dummyData";

読み込んだダミーデータをプルダウンメニューコンポーネントの「options」に渡します。
「options」は文字列型のvalueとlabelプロパティしか持たないオブジェクトなので、mapメソッドでダミーデータから必要な値を取り出します。取り出した値はスプレッド演算子で展開して追加します。

初期値を設定したかったので、「options」の最初に{ value: “”, label: “言語を選択してください” }を追加しています。

また、先ほど作成したイベントをonChangeイベントに設定します。引数は選択したvalue値です。
こちらが作成した言語のプルダウンメニューです。

言語のプルダウンメニュー

<PullDown
  options={[
    { value: "", label: "言語を選択してください" },
    ...methodData.map((data) => ({
      value: data.language,
      label: data.label,
    })),
  ]}
  onChange={(e) => handleLanguageChange(e.currentTarget.value)}
/>

フレームワークのプルダウンメニュー

まず、ダミーデータから選択した言語のフレームワークを取得する必要があります。
値を取得するには、findメソッドで選択された言語のフレームワークを探し、見つかったら、mapメソッドでデータを取り出します。言語が見つからなかったら、空の配列[]を返します。

<PullDown
  options={[
    { value: "", label: "フレームワークを選択してください。" },
    ...(
      (selectedLanguage &&
        methodData.find((data) => data.language === selectedLanguage)
          ?.frameworks) || []
    ).map((data) => ({
      value: data.name,
      label: data.label,
    })),
  ]}
  onChange={(e) => handleFrameworkChange(e.currentTarget.value)}
/>

バージョンのプルダウンメニュー

バージョンもフレームワークの取得と同じ考えです。
findメソッドで選択された言語とフレームワークを探し、見つからなかったら、空の配列[]を返します。

<PullDown
  options={[
    { value: "", label: "バージョンを選択してください。" },
    ...(
      (selectedFramework &&
        methodData
          .find((data) => data.language === selectedLanguage)
          ?.frameworks.find((fw) => fw.name === selectedFramework)
          ?.versions) ||
      []
    ).map((version) => ({
      value: version,
      label: version,
    })),
  ]}
  onChange={(e) => handleVersionChange(e.currentTarget.value)}
/>

以上で連動するプルダウンメニューが完成しました!

まとめ

useStateの値が変更するたび画面がレンダリングされるので、この事を前提とした作成方法となります。素のJavaScriptとは作り方が随分と異なるので、最初はどうやって作ったら良いかわからなかったのです。
Reactのレンダリングの仕組みが分かればなるほどなぁー。という感じです。