【React】コンボボックスを作ってみた

# はじめに
前回は、簡単な入力フォームをReactにて作成しました。

https://qiita.com/Y_Hakiai/items/6573e53cf8d57b413d04

今回は、プルダウンのコンボボックスを作ってみます。
また、Material-UIのライブラリも併せて使ってみたいと思います。

# 環境・バージョン情報
* Visual Studio Code v1.70.1
* Node.js v16.13.2
* npm v8.11.0
* react v17.0.2
* react-dom v17.0.2
* typescript v4.7.4,
* material-ui/core v4.12.3

# create-react-app
1 .前回同様、'create-react-app'でプロジェクトを作成します。
今回は、typescriptを利用する為、'--template typescript'を指定します。
```
npx create-react-app combo-box --template typescript
```
2 .実行完了後、package.jsonを以下の通り編集します。
今回は、material-ui/core v4.12.3を使用する為、'react v17.0.2'を使用します。

:::note warn
'material-ui v4.12.3'はreact v18では使用出来ない。
:::
* react v17.0.2に変更
* testing-libraryを削除（今回は不要な為）
```diff_json:package.json
{
  "name": "combo-box",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
-   "@testing-library/jest-dom": "^5.16.5",
-   "@testing-library/react": "^13.3.0",
-   "@testing-library/user-event": "^13.5.0",
+   "@material-ui/core": "^4.12.3",
    "@types/jest": "^27.5.2",
    "@types/node": "^16.11.48",
-   "@types/react": "^18.0.17",
+   "@types/react": "^17.0.0",
-   "@types/react-dom": "^18.0.6",
+   "@types/react-dom": "^17.0.0",
-   "react": "^18.2.0",
+   "react": "^17.0.2",
-   "react-dom": "^18.2.0",
+   "react-dom": "^17.0.2",
    "react-scripts": "5.0.1",
    "typescript": "^4.7.4",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

```

3 .以下ファイルを削除します。（testing-libraryを削除した為）
* src/App.test.tsx

4 .'package-lock.json'及び'node_modules/'を削除してnpm installを実行します。
　※プロジェクト配下にディレクトリを移動してから、実行するようにする。
```
npm install
```

5 .'src/index.tsx'を以下の通り修正します。
```diff_tsx: index.tsx
+import { render } from "react-dom";
-import React from 'react';
-import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

+const rootElement = document.getElementById("root");
+render(<App />, rootElement);

-const root = ReactDOM.createRoot(
-  document.getElementById('root') as HTMLElement
-);
-root.render(
-  <React.StrictMode>
-    <App />
-  </React.StrictMode>
-);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
```

6 .'npm start'を実行します。
```
npm start
```
無事にいつもの画面が表示されました。
![WS000000.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2638718/4a1bd203-7d93-90af-7882-f62e2dde693f.jpeg)

# コンボボックス作成
今回は、'App.tsx'内にコンボボックスを作成しました。
* 完成した画面
都道府県のプルダウンを変更すると、その都道府県に紐づく市区町村のプルダウンが生成されます。
![WS000001.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2638718/9ac0c660-5a9d-a7d5-1243-8d9f2ea5723c.jpeg)

* ソースコード
```tsx:index.tsx
import { render } from "react-dom";
import './index.css';
import App from "./App";
import reportWebVitals from './reportWebVitals';

const rootElement = document.getElementById("root");
render(<App />, rootElement);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
```

※前回同様、INPUTデータはJSONで受け取ることを想定して作ってみました！
```tsx:App.tsx
import React, { useState, useRef } from "react";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import InputLabel from "@material-ui/core/InputLabel";
import './App.css';

interface DataList {
  prefecturesId: string;
  prefecturesName: string;
  city: {
    cityId: string;
    cityName: string;
  }[];
}

interface ComboBoxItem {
  id: string;
  value: string;
}

const InputData = [
  {
    prefecturesId: "1",
    prefecturesName: "東京都",
    city: [
      {
        cityId: "1",
        cityName: "立川市",
      },
      {
        cityId: "2",
        cityName: "八王子市",
      },
      {
        cityId: "3",
        cityName: "調布市",
      },
      {
        cityId: "4",
        cityName: "青梅市",
      },
      {
        cityId: "5",
        cityName: "世田谷区",
      },
      {
        cityId: "6",
        cityName: "江東区",
      },
      {
        cityId: "7",
        cityName: "品川区",
      },
    ],
  },
  {
    prefecturesId: "2",
    prefecturesName: "神奈川県",
    city: [
      {
        cityId: "1",
        cityName: "横須賀市",
      },
      {
        cityId: "2",
        cityName: "横浜市",
      },
      {
        cityId: "3",
        cityName: "川崎市",
      },
      {
        cityId: "4",
        cityName: "逗子市",
      },
    ],
  },
];

// INPUT用JSONデータ作成
const Data = JSON.stringify(InputData);

const App = () => {
  // JSON形式のINPUTデータを取り出し
  const data = JSON.parse(Data);
  const inputData: DataList[] = Object.keys(data).map(function (key) {
    return data[key];
  });

  // 都道府県リストState管理
  const [prefecturesOptions] = useState<ComboBoxItem[]>(
    inputData.map((d) => {
      return {
        id: d.prefecturesId,
        value: d.prefecturesName,
      };
    })
  );
  // 都道府県リストで選択中の都道府県ID State管理
  const [selectedPrefecturesId, setSelectedPrefecturesId] = useState<string>(
    inputData[0].prefecturesId
  );
  // 都道府県（選択中）Ref管理
  const prefecturesOptionsRef = useRef(
    inputData
      .filter((d) => d.prefecturesId === selectedPrefecturesId)[0]
      .city.map((d) => {
        return {
          id: d.cityId,
          value: d.cityName,
        };
      })
  );

  // 市区町村リストState管理
  const [selectedCityId, setSelectedCityId] = useState(
    inputData[0].city[0].cityId
  );

  // 都道府県リスト変更時
  const onPrefecturesChangeHandler = (prefecturesId: string) => {
    //選択した都道府県をStateに設定
    setSelectedPrefecturesId(prefecturesId);

    //選択した都道府県の市区町村一覧
    const selectedCity = inputData.filter(
      (d) => d.prefecturesId === prefecturesId
    )[0].city;

    //選択した都道府県の市区町村リストの先頭を指定
    setSelectedCityId(selectedCity[0].cityId);

    //選択した都道府県の市区町村をRefに指定
    prefecturesOptionsRef.current = selectedCity.map((d) => {
      return {
        id: d.cityId,
        value: d.cityName,
      };
    });
  };

  return (
    <div className="div">
      <FormControl className="form-width">
        <InputLabel>都道府県</InputLabel>
        <Select
          defaultValue={prefecturesOptions[0].id}
          value={selectedPrefecturesId}
          onChange={(e) => {
            if (e.target.value !== undefined) {
              onPrefecturesChangeHandler(e.target.value as string);
            }
          }}
        >
          {prefecturesOptions.map((item) => (
            <MenuItem value={item.id} key={item.id}>
              {item.value}
            </MenuItem>
          ))}
        </Select>
      </FormControl>
      <div className="spacer"></div>
      <FormControl className="form-width">
        <InputLabel>市区町村</InputLabel>
        <Select
          defaultValue={"1"}
          value={selectedCityId}
          onChange={(e) => {
            if (e.target.value !== undefined) {
              setSelectedCityId(e.target.value as string);
            }
          }}
        >
          {prefecturesOptionsRef.current.map((item) => (
            <MenuItem value={item.id} key={item.id}>
              {item.value}
            </MenuItem>
          ))}
        </Select>
      </FormControl>
    </div>
  );
};

export default App;
```

```css:App.css
/* 全体 */
.div {
  margin: 25px;
}

/* プルダウン間隔 */
.spacer {
  margin-top: 15px;
}

/* プルダウン幅 */
.form-width {
  width: 200px;
}
```
# まとめ
今回、'useState','useRef'を実際に使ってみて使い方がイメージでき、
'Material-UI'についても、知見を深めることが出来ました。
Typescriptに関しては、型宣言が出来るのはやはり便利だと痛感しました。

また、本来、今回作成したコンボボックスは、'FormControl'タグ以降をコンポーネント化して利用するのが、
Reactの正しい書き方だと思います。※今回は、1ファイル（"App.tsx"）に記載しています。
コンポーネント化の粒度等も今後勉強していきたいと思いました。
※今回、参考にさせて頂きましたサイトの方はコンボボックスが、コンポーネント化されています。

# 参考資料
* [【React】Material-UIがインストールできない](https://qiita.com/shiho97797/items/cacb21d09330de56a505)
* [[Material-UI] Select Componentで依存関係のある2つのコンボボックスを実装してみた](https://dev.classmethod.jp/articles/two-comboboxes-with-dependencies-using-material-ui-select-component/)