はじめに
前回までの記事の続きです。表題通り、tanstackqueryを使って実現します!
成果物
ソースコード
ディレクトリ構成
~/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>
);
};