ReactHookFormの項目間で機能連動する(Step2：プルダウンの連動 API)

## はじめに
[前回](https://qiita.com/koji0705/items/8e849885061ea1ca1350)までの記事の続きです。前回までは項目連動を実施しました。
今回は取得する項目が可変な想定で、API通信を実施しプルダウンの一覧を取得するようにします。

## 成果物

![rhf_step2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/d2e02d33-4a2a-487f-df2b-7e3b97377acf.gif)
ちょっとわかりづらいですが、大分類を変更する度に小分類の一覧を取得し直しています。

## 準備
API連動のため、serverが必要となります。本質と異なる箇所なので、下記に折りたたんでおきます。

<details><summary>serverの準備</summary>

```js:server.js

import express from "express";
const app = express();
const port = 4000;

const categories = [
	{ id: 1, key: "food", name: "食べ物" },
	{ id: 2, key: "animal", name: "動物" },
	{ id: 3, key: "vehicle", name: "乗り物" },
];

const subCategories = {
	1: [
		{ id: 1, key: "apple", name: "りんご" },
		{ id: 2, key: "banana", name: "バナナ" },
		{ id: 3, key: "orange", name: "オレンジ" },
	],
	2: [
		{ id: 1, key: "dog", name: "犬" },
		{ id: 2, key: "cat", name: "猫" },
		{ id: 3, key: "rabbit", name: "うさぎ" },
	],
	3: [
		{ id: 1, key: "car", name: "車" },
		{ id: 2, key: "bike", name: "自転車" },
		{ id: 3, key: "bus", name: "バス" },
	],
};

app.get("/api/category", (req, res) => {
	res.json(categories);
});

app.get("/api/category/:id", (req, res) => {
	const subCategory = subCategories[req.params.id];
	if (subCategory) {
		res.json(subCategory);
	} else {
		res.status(404).send("Not found");
	}
});

app.listen(port, () => {
	console.log(`Mock API server running at http://localhost:${port}`);
});
```

```shell:serverの実行
~/develop/HITOTSU/rhf_autocomplete/src/mock-server  (feat/auto_complete)$ node server.js
Mock API server running at http://localhost:4000
```
</details>

## ソースコード
```shell:ディレクトリ構成
~/develop/HITOTSU/rhf_autocomplete  (feat/auto_complete)$ tree src/form/
src/form/
├── Form.tsx
├── api.ts
├── hooks.ts
└── type.ts

1 directory, 4 files
```


```tsx:src/form/Form.tsx
import type React from "react";
import { Controller } from "react-hook-form";
import { Select, MenuItem, FormControl, InputLabel } from "@mui/material";
import { useCategoryForm } from "./hooks";

export const FormWithDependentSelects: React.FC = () => {
	const { control, categories, subCategoryOptions, selectedCategory } =
		useCategoryForm();

	return (
		<form>
			{/* 大分類のプルダウン */}
			<FormControl fullWidth>
				<InputLabel id="category-label">大分類</InputLabel>
				<Controller
					name="category"
					control={control}
					render={({ field }) => (
						<Select {...field} labelId="category-label" label="大分類">
							<MenuItem value="">
								<em>選択してください</em>
							</MenuItem>
							{categories.map((category) => (
								<MenuItem key={category.id} value={category.id}>
									{category.name}
								</MenuItem>
							))}
						</Select>
					)}
				/>
			</FormControl>

			{/* 小分類のプルダウン */}
			<FormControl fullWidth style={{ marginTop: "16px" }}>
				<InputLabel id="subCategory-label">小分類</InputLabel>
				<Controller
					name="subCategory"
					control={control}
					render={({ field }) => (
						<Select
							{...field}
							labelId="subCategory-label"
							label="小分類"
							disabled={!selectedCategory} // 大分類が選ばれていないときは無効化
						>
							<MenuItem value="">
								<em>選択してください</em>
							</MenuItem>
							{subCategoryOptions.map((subCategory) => (
								<MenuItem key={subCategory.id} value={subCategory.id}>
									{subCategory.name}
								</MenuItem>
							))}
						</Select>
					)}
				/>
			</FormControl>
		</form>
	);
};
```

```ts:src/form/api.ts
import axios from "axios";
import type { Category, SubCategory } from "./type";

const baseUrl = "http://localhost:3000/api";

export const getCategories = async () => {
	const response = await axios.get<Category[]>(`${baseUrl}/category`);
	return response.data;
};

export const getSubCategories = async (categoryId: number) => {
	const response = await axios.get<SubCategory[]>(
		`${baseUrl}/category/${categoryId}`,
	);
	return response.data;
};
```

```ts:src/form/hooks.ts
import { useEffect, useState, useCallback } from "react";
import { useForm } from "react-hook-form";
import type { Category, SubCategory } from "./type";
import { getCategories, getSubCategories } from "./api";

export const useCategoryForm = () => {
	const { control, watch, setValue } = useForm({
		defaultValues: {
			category: "",
			subCategory: "",
		},
	});

	const [categories, setCategories] = useState<Category[]>([]);
	const [subCategoryOptions, setSubCategoryOptions] = useState<SubCategory[]>(
		[],
	);
	const selectedCategory = watch("category");

	// 大分類データの取得
	const fetchCategories = useCallback(async () => {
		const response = await getCategories();
		// const response = await axios.get<Category[]>(
		// 	"http://localhost:3000/api/category",
		// );
		setCategories(response);
	}, []); // useCallbackでメモ化

	// 小分類データの取得
	const fetchSubCategories = useCallback(async (categoryId: number) => {
		const response = await getSubCategories(categoryId);
		setSubCategoryOptions(response);
	}, []);

	// 初回レンダリング時に大分類を取得
	useEffect(() => {
		fetchCategories();
	}, [fetchCategories]); // 依存配列にfetchCategoriesを追加

	// 大分類が変更されたときに小分類を取得
	useEffect(() => {
		if (selectedCategory) {
			fetchSubCategories(Number(selectedCategory));
			setValue("subCategory", ""); // 小分類をリセット
		}
	}, [selectedCategory, fetchSubCategories, setValue]); // fetchSubCategoriesを依存配列に追加

	return {
		control,
		categories,
		subCategoryOptions,
		selectedCategory,
	};
};
```

```ts:src/form/type.ts
// カテゴリの型定義
export interface Category {
	id: number;
	key: string;
	name: string;
}

export interface SubCategory {
	id: number;
	key: string;
	name: string;
}
```

## 最後に
できましたね！カテゴリー内でごにょごにょ操作しないといけないかなぁと思ってたんですが、カスタムフック内で処理が閉じれてよかったです。
次回は検索機能を追加したいため、AutoCompleteと連動しようと思っています！