ReactHookFormとMUIのAutoCompleteで、プルダウン検索を実施できる(Step3: tanstackqueryを使う)

## はじめに
[前回](https://qiita.com/koji0705/items/e086b8fdd47efba39e6f)までの記事の続きです。表題通り、tanstackqueryを使って実現します！

## 成果物

![rhf_tanstack.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/dbf66b39-6e6b-8214-6112-926f41b6cdf5.gif)

## ソースコード
```shell:ディレクトリ構成
~/develop/HITOTSU/rhf_autocomplete  (feat/auto_complete_tanstack)$ tree ./src/
./src/
├── App.tsx
├── form
│   ├── Form.tsx
│   ├── api.ts
│   ├── hooks.ts
│   └── type.ts
├── main.tsx
├── mock-server
│   └── server.js
└── vite-env.d.ts
```

```tsx:src/App.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { SearchAutocomplete } from "./form/Form";

function App() {
	const queryClient = new QueryClient();
	return (
		<QueryClientProvider client={queryClient}>
			<SearchAutocomplete />
		</QueryClientProvider>
	);
}

export default App;
```

```ts:src/form/api.ts
import axios from "axios";
import type {
	Category,
	PullDownOption,
	SubCategory,
	UserResponse,
} 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;
};

export const fetchUserOptions = async (
	query?: string,
): Promise<PullDownOption[]> => {
	const response = await axios.get<UserResponse[]>(
		"https://jsonplaceholder.typicode.com/users",
		{
			params: { q: query },
		},
	);
	return response.data.map((item) => ({
		label: item.name,
		value: item.id,
	}));
};
```

```tsx:src/form/Form.tsx
import React, { useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { TextField, Autocomplete } from "@mui/material";
import { useUserOptions } from "./hooks";
import type { FormValues } from "./type";

// ユーザー検索のためのAutocompleteコンポーネント
export const SearchAutocomplete = () => {
	const { control, handleSubmit } = useForm<FormValues>({
		defaultValues: {
			user: null,
		},
	});
	// APIのクエリKey
	const [searchQuery, setSearchQuery] = useState("");

	// カスタムフックを使ってクエリを実行
	const { data: options = [], isLoading } = useUserOptions(searchQuery);

	// ユーザーが入力した際にAPIを呼び出す関数
	const handleInputChange = (value: string) => {
		setSearchQuery(value); // 検索クエリを更新し、クエリを再実行
	};

	// フォームの送信処理
	const onSubmit = (data: FormValues) => {
		console.log("選択されたデータ:", data);
	};

	return (
		<form onSubmit={handleSubmit(onSubmit)}>
			<Controller
				name="user"
				control={control}
				render={({ field }) => (
					<Autocomplete
						{...field}
						sx={{ width: 300 }}
						options={options}
						getOptionLabel={(option) => option.label}
						loading={isLoading}
						onInputChange={(event, value) => handleInputChange(value)} // ユーザー入力時にAPIを呼び出し
						renderInput={(params) => (
							<TextField
								{...params}
								label="ユーザー検索"
								variant="outlined"
								InputProps={{
									...params.InputProps,
								}}
							/>
						)}
						onChange={(event, value) => field.onChange(value)} // 選択時にフィールドを更新
					/>
				)}
			/>
		</form>
	);
};
```

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

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();
		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,
	};
};

// カスタムフック: ユーザーのオプションを取得
export const useUserOptions = (searchQuery: string) => {
	return useQuery({
		queryKey: ["users", searchQuery], // クエリキーを渡す
		queryFn: () => fetchUserOptions(searchQuery), // クエリ関数
	});
};
```

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

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

export interface UserResponse {
	id: number;
	name: string;
}

export type PullDownOption = {
	label: string;
	value: number;
};

export type FormValues = {
	user: { label: string; value: number } | null;
};
```

## おわりに
ReactHookFormのwatchを使えばもっと綺麗に書けそうな気がしています。
useStateは極力使わない実装が良いなと思っています！
→下記で実装できました！


## Appendix
```tsx:
import React from "react";
import { useForm, Controller } from "react-hook-form";
import { TextField, Autocomplete } from "@mui/material";
import { useUserOptions } from "./hooks";
import type { FormValues } from "./type";

// ユーザー検索のためのAutocompleteコンポーネント
export const SearchAutocomplete = () => {
	const { control, handleSubmit, watch } = useForm<FormValues>({
		defaultValues: {
			user: null,
		},
	});

	// watchを使ってuserフィールドの値を監視
	const searchQuery = watch("user")?.label || ""; // userのラベル（検索文字列）を取得

	// カスタムフックを使ってクエリを実行
	const { data: options = [], isLoading } = useUserOptions(searchQuery);

	// フォームの送信処理
	const onSubmit = (data: FormValues) => {
		console.log("選択されたデータ:", data);
	};

	return (
		<form onSubmit={handleSubmit(onSubmit)}>
			<Controller
				name="user"
				control={control}
				render={({ field }) => (
					<Autocomplete
						{...field}
						sx={{ width: 300 }}
						options={options}
						getOptionLabel={(option) => option.label}
						loading={isLoading}
						// onInputChangeの処理はwatchで代替されるので削除可能
						renderInput={(params) => (
							<TextField
								{...params}
								label="ユーザー検索"
								variant="outlined"
								InputProps={{
									...params.InputProps,
								}}
							/>
						)}
						onChange={(event, value) => field.onChange(value)} // 選択時にフィールドを更新
					/>
				)}
			/>
		</form>
	);
};
```