👻

ReactHookFormとMUI AutoCompleteで複数のテキストフィールドを扱う

2024/10/12に公開

はじめに

表題通り、ReactHookFormとMUI AutoCompleteで複数のテキストフィールドを扱います。

成果物

ソースコード

src/MultipleSearchAutocomplete.tsx
import React, { useEffect, useState } from "react";
import { useForm, Controller, type Control } from "react-hook-form";
import { TextField, Autocomplete } from "@mui/material";
import axios from "axios";

type FormValues = {
	user: { label: string; value: number };
	product: { label: string; value: number };
};

type Option = {
	label: string;
	value: number;
};

// APIからデータを取得する汎用関数
const fetchOptions = async <
	T extends { id: number; name?: string; title?: string },
>(
	url: string,
	query?: string,
	labelKey: keyof T = "name", // デフォルトを "name" に設定
): Promise<Option[]> => {
	try {
		const response = await axios.get<T[]>(url, { params: { q: query } });
		return response.data.map((item) => ({
			label: String(item[labelKey]),
			value: item.id,
		}));
	} catch (error) {
		console.error("Error fetching data:", error);
		return [];
	}
};

// Autocomplete 共通コンポーネント
const SearchAutocomplete = ({
	name,
	control,
	label,
	options,
	onInputChange,
}: {
	name: keyof FormValues;
	control: Control<FormValues>;
	label: string;
	options: Option[];
	onInputChange: (value: string) => void;
}) => (
	<Controller
		name={name}
		control={control}
		render={({ field }) => (
			<Autocomplete
				{...field}
				sx={{ width: 300 }}
				options={options}
				getOptionLabel={(option) => option?.label || ""} // オプションのラベルを取得
				onInputChange={(event, value) => onInputChange(value)} // ユーザー入力時の処理
				renderInput={(params) => (
					<TextField {...params} label={label} variant="outlined" />
				)}
				onChange={(event, value) => field.onChange(value)} // 選択時にフィールドを更新
				clearOnEscape // Escapeキーでクリア可能
				clearText="クリア" // バツボタンのラベルを設定
			/>
		)}
	/>
);

export const MultipleSearchAutocomplete = () => {
	const { control } = useForm<FormValues>();

	const [userOptions, setUserOptions] = useState<Option[]>([]);
	const [productOptions, setProductOptions] = useState<Option[]>([]);

	// 初回ロード時に全オプションを取得
	useEffect(() => {
		const loadInitialOptions = async () => {
			const initialUserOptions = await fetchOptions(
				"https://jsonplaceholder.typicode.com/users",
			);
			const initialProductOptions = await fetchOptions(
				"https://jsonplaceholder.typicode.com/posts",
				undefined,
				"title",
			);
			setUserOptions(initialUserOptions);
			setProductOptions(initialProductOptions);
		};
		loadInitialOptions();
	}, []);

	// ユーザー検索用の入力処理
	const handleUserInputChange = async (value: string) => {
		const filteredOptions = value
			? await fetchOptions("https://jsonplaceholder.typicode.com/users", value)
			: await fetchOptions("https://jsonplaceholder.typicode.com/users"); // クリア時に全ユーザーを表示
		setUserOptions(filteredOptions);
	};

	// プロダクト検索用の入力処理
	const handleProductInputChange = async (value: string) => {
		const filteredOptions = value
			? await fetchOptions(
					"https://jsonplaceholder.typicode.com/posts",
					value,
					"title",
				)
			: await fetchOptions(
					"https://jsonplaceholder.typicode.com/posts",
					undefined,
					"title",
				); // クリア時に全プロダクトを表示
		setProductOptions(filteredOptions);
	};

	return (
		<form>
			{/* ユーザー検索用オートコンプリート */}
			<SearchAutocomplete
				name="user"
				control={control}
				label="ユーザー検索"
				options={userOptions}
				onInputChange={handleUserInputChange}
			/>

			{/* プロダクト検索用オートコンプリート */}
			<SearchAutocomplete
				name="product"
				control={control}
				label="プロダクト検索"
				options={productOptions}
				onInputChange={handleProductInputChange}
			/>
		</form>
	);
};

Discussion