0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-10-15

はじめに

前回までの記事の続きです。表題通り、tanstackqueryを使って実現します!

成果物

rhf_tanstack.gif

ソースコード

ディレクトリ構成
~/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
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;
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,
	}));
};
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>
	);
};
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), // クエリ関数
	});
};
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

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>
	);
};
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?