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の連動

Posted at

はじめに

表題通りです。

成果物

react_hook_form_render.gif

ソースコード

src/UpdateProduct.tsx
import type React from "react";
import { Suspense } from "react";
import { Button } from "@mui/material";
import { SearchAutocomplete } from "./form/Form";
import { useProductQuery, useSearchAutocomplete } from "./form/hooks";
import type { Product } from "./form/type";

const PRODUCT_ID = 1;

// 製品データ取得後に表示するコンポーネント
const UpdateProductComponent = ({ product }: { product: Product }) => {
	const {
		control,
		categoryOptions,
		isCategoryLoading,
		subCategoryOptions,
		isSubCategoryLoading,
		setValue,
		dirtyFields,
		onSubmit,
		onCancel,
	} = useSearchAutocomplete(product);

	return (
		<div>
			<h1>UpdateProduct</h1>
			<SearchAutocomplete
				control={control}
				categoryOptions={categoryOptions}
				isCategoryLoading={isCategoryLoading}
				subCategoryOptions={subCategoryOptions}
				isSubCategoryLoading={isSubCategoryLoading}
				setValue={setValue}
			/>
			{dirtyFields && (
				<div>
					{Object.keys(dirtyFields).length > 0
						? "以下のフィールドが変更されました:"
						: "変更はありません"}
					<ul>
						{Object.keys(dirtyFields).map((field) => (
							<li key={field}>{field}</li>
						))}
					</ul>
					<Button
						onClick={onSubmit}
						disabled={Object.keys(dirtyFields).length === 0}
						sx={{
							width: 300,
							backgroundColor:
								Object.keys(dirtyFields).length === 0 ? "gray" : "blue", // disabledの時は灰色
							color: "white",
							"&:hover": {
								backgroundColor:
									Object.keys(dirtyFields).length === 0 ? "gray" : "darkblue", // hover時の色も設定
							},
						}}
					>
						保存
					</Button>
					<Button onClick={onCancel}>キャンセル</Button>
				</div>
			)}
		</div>
	);
};

// Suspenseによるローディング処理のラップ
export const UpdateProduct = () => {
	return (
		<Suspense fallback={<div>Loading...</div>}>
			<ProductLoader />
		</Suspense>
	);
};

// 非同期に製品をロードしてから表示するコンポーネント
const ProductLoader: React.FC = () => {
	const { data: product, isLoading } = useProductQuery(PRODUCT_ID);

	if (isLoading || !product) {
		return <div>Loading...</div>; // ローディング状態の表示
	}

	return <UpdateProductComponent product={product} />;
};
src/form/Form.tsx
import {
	type Control,
	Controller,
	type UseFormSetValue,
} from "react-hook-form";
import { TextField, Autocomplete } from "@mui/material";
import type { Category, FormValues } from "./type";

type Props = {
	control: Control<FormValues, unknown>;
	categoryOptions: Category[];
	isCategoryLoading: boolean;
	subCategoryOptions: Category[];
	isSubCategoryLoading: boolean;
	setValue: UseFormSetValue<FormValues>;
};

// ユーザー検索のためのAutocompleteコンポーネント
export const SearchAutocomplete = ({
	control,
	categoryOptions,
	isCategoryLoading,
	subCategoryOptions,
	isSubCategoryLoading,
	setValue,
}: Props) => {
	return (
		<form>
			{/* 親カテゴリーのAutocomplete */}
			<Controller
				name="category"
				control={control}
				render={({ field }) => (
					<Autocomplete
						{...field}
						sx={{ width: 300 }}
						options={categoryOptions}
						getOptionLabel={(option: Category) => option.name} // nameフィールドを表示
						loading={isCategoryLoading}
						value={field.value}
						onChange={(event, value) => {
							field.onChange(value); // 親カテゴリー選択時にフィールドを更新
							setValue("subCategory", null); // 親カテゴリー変更時にサブカテゴリーをリセット
						}}
						renderInput={(params) => (
							<TextField
								{...params}
								label="親カテゴリー検索"
								variant="outlined"
								InputProps={{
									...params.InputProps,
								}}
							/>
						)}
					/>
				)}
			/>

			{/* サブカテゴリーのAutocomplete */}
			<Controller
				name="subCategory"
				control={control}
				render={({ field }) => (
					<Autocomplete
						{...field}
						sx={{ width: 300, marginTop: "16px" }}
						options={subCategoryOptions}
						getOptionLabel={(option: Category) => option.name} // nameフィールドを表示
						loading={isSubCategoryLoading}
						value={field.value}
						onInputChange={(event, value) => {
							if (field.value) {
								setValue("subCategory", {
									...field.value,
									name: value,
									id: field.value.id ?? 0,
								});
							}
						}} // サブカテゴリーの入力変更時にsearchNameを更新
						onChange={(event, value) => field.onChange(value)} // サブカテゴリー選択時にフィールドを更新
						renderInput={(params) => (
							<TextField
								{...params}
								label="サブカテゴリー検索"
								variant="outlined"
								InputProps={{
									...params.InputProps,
								}}
							/>
						)}
					/>
				)}
			/>
		</form>
	);
};
src/form/hooks.ts
import { useQuery } from "@tanstack/react-query";
import { fetchCategories, fetchProduct, fetchSubCategories } from "./api";
import type { FormValues, Product } from "./type";
import { useForm } from "react-hook-form";

// カスタムフック: カテゴリのオプションを取得
export const useCategoryOptions = (searchName: string) => {
	return useQuery({
		queryKey: ["categories", searchName], // クエリキーをsearchNameに基づいて更新
		queryFn: () => fetchCategories(searchName), // クエリ関数
		enabled: true, // 初期ロード時もクエリを実行
	});
};

// サブカテゴリのオプションを取得するカスタムフック
export const useSubCategoryOptions = (
	categoryId: number | null,
	searchName: string,
) => {
	return useQuery({
		queryKey: ["subCategories", categoryId, searchName], // categoryIdとsearchNameに基づいてクエリを実行
		queryFn: () => {
			if (categoryId === null) {
				return Promise.resolve([]); // categoryIdがnullの場合は空配列を返す
			}
			return fetchSubCategories(categoryId, searchName); // サブカテゴリーのAPI呼び出し
		},
		enabled: !!categoryId, // categoryIdがnullでない場合のみ実行
	});
};

// 製品データを取得するカスタムフック
export const useProductQuery = (productId: number) => {
	return useQuery({
		queryKey: ["product", productId], // 製品IDに基づいてクエリを実行
		queryFn: () => fetchProduct(productId), // 製品データの取得
	});
};

export const useSearchAutocomplete = (product: Product) => {
	const {
		control,
		watch,
		setValue,
		formState: { dirtyFields },
		reset,
	} = useForm<FormValues>({
		defaultValues: {
			category: product.category,
			subCategory: product.subCategory,
		},
	});

	// 親カテゴリーの監視
	const selectedCategory = watch("category");
	const categoryId = selectedCategory?.id || null;

	// 親カテゴリーの検索に基づくオプションを取得
	const { data: categoryOptions = [], isLoading: isCategoryLoading } =
		useCategoryOptions(selectedCategory?.name || "");

	// サブカテゴリー検索のための監視
	const subCategorySearchName = watch("subCategory")?.name || "";

	// サブカテゴリーのオプションを取得
	const { data: subCategoryOptions = [], isLoading: isSubCategoryLoading } =
		useSubCategoryOptions(categoryId, subCategorySearchName);

	const onSubmit = () => {
		console.log("onSubmit", dirtyFields);
	};

	const onCancel = () => {
		reset(); // フォームの値を初期値にリセット
	};

	return {
		control,
		categoryOptions,
		isCategoryLoading,
		subCategoryOptions,
		isSubCategoryLoading,
		setValue,
		dirtyFields,
		onSubmit,
		onCancel,
	};
};
src/form/type.ts
export type Category = {
	id: number;
	key: string;
	name: string;
};

// FormValuesの型定義
export type FormValues = {
	category: Category | null; // 親カテゴリー
	subCategory: Category | null; // サブカテゴリー
};

// 製品の型
export type Product = {
	id: number;
	category: Category;
	subCategory: Category;
};

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?