1
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?

[Next.js]React Hook FormとMUIでカスタム検索フックスを作成してみる

Last updated at Posted at 2024-10-05

使用技術

  • typescript
  • Next.js
  • MUI
  • react hook form

概要

react hook formとMUIを使って検索フォームを作成するときの検索用のカスタムフックスを作ってみました。

内容

バックエンドの検索APIに対してリクエストを送る場合を考えます

作成した検索フックス

引数は初期値とバックエンドへのリクエスト用のサーバーアクションメソッドです。
型引数にはreact hook formのフォームの型と検索結果の型を入れます。
今回はreact hook formの型とAPIのリクエストの型が同じで、検索結果の型とAPIのレスポンスの型が同じ場合で考えました。
型が違う場合は変換処理を検索フックス内に記述するか、引数としてメソッドで渡す必要があります。
初期値についてはサーバコンポーネント側で取得することが多いと思うので、引数として追加しました。
検索フックスの返り値はuseFormの値を丸ごと返しています。
また、isLoadingやerrorも返すことで各コンポーネントで定義せずに簡単にハンドリングできるようになりました。
下のコードではそれぞれの動きの確認用に、遅延処理とエラー発生用のコードも記載しています。
APIのレスポンスに対してのエラーハンドリングに関しては今回は行いませんでした。

useSearch.ts
import { useState } from "react";
import { FieldValues, useForm } from "react-hook-form";

interface useSearchProps<TRequest extends FieldValues, TResponse> {
  defaultValue: TResponse;
  searchServerAction: (requestBody: TRequest) => Promise<TResponse>;
}
const useSearch = <TRequest extends FieldValues, TResponse>(
  props: useSearchProps<TRequest, TResponse>
) => {
  const { defaultValue, searchServerAction } = props;
  const form = useForm<TRequest>();
  const [results, setResults] = useState<TResponse | undefined>(defaultValue);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string>();
  //   フォームの型とリクエストの型が異なる場合は変換する必要がある
  const search = form.handleSubmit(async (data: TRequest) => {
    setIsLoading(true);
    setError(undefined);
    setResults(undefined);
    try {
      // 本来はエラーハンドリングを追加
      const searchResults = await searchServerAction(data);
      //   確認用
      //   throw new Error();
      setResults(searchResults);

      // eslint-disable-next-line @typescript-eslint/no-unused-vars
    } catch (err) {
      setError("failed");
    } finally {
      setIsLoading(false);
    }
  });
  return { form, search, results, isLoading, error };
};
export default useSearch;

検索フックスを呼び出す

page.tsxはサーバコンポーネントとして、初期データを取得しています。
その初期データをクライアントコンポーネントであるBookSearchFormに渡しています。
検索部分に関してはモック化しています。
searchメソッドが実行されるとresultsが変更されます。

page.tsx
import { books } from "@/const/const";
import { SearchBooksResponse } from "@/serverActions/searchBooks";
import BookSearchForm from "./BookSearchForm";
import Box from "@mui/material/Box";

export default function Home() {
  // 本来はAPIからデータを取得
  const res: SearchBooksResponse = { status: "200", data: books };
  return (
    <div>
      <Box
        sx={{
          display: "flex",
          justifyContent: "center",
          alignItems: "center",
          minHeight: "100vh",
        }}
      >
        <BookSearchForm defaultValue={res} />
      </Box>
    </div>
  );
}
BookSearchForm.tsx
"use client";
import React from "react";
import { Controller } from "react-hook-form";
import {
  TextField,
  Button,
  Stack,
  List,
  ListItem,
  Typography,
  CircularProgress,
  Alert,
} from "@mui/material";
import useSearch from "@/hooks/useSearch";
import searchBooks, {
  SearchBooksRequest,
  SearchBooksResponse,
} from "@/serverActions/searchBooks";

interface BookSearchFormProps {
  defaultValue: SearchBooksResponse;
}

const BookSearchForm: React.FC<BookSearchFormProps> = (props) => {
  const { defaultValue } = props;
  const { form, search, results, isLoading, error } = useSearch<
    SearchBooksRequest,
    SearchBooksResponse
  >({ defaultValue: defaultValue, searchServerAction: searchBooks });
  const { control } = form;
  return (
    <div>
      <form onSubmit={search}>
        <Stack direction="row" spacing={2}>
          <Controller
            name="title"
            control={control}
            defaultValue=""
            render={({ field }) => (
              <TextField
                {...field}
                label="本の名前"
                variant="outlined"
                fullWidth
              />
            )}
          />
          <Controller
            name="author"
            control={control}
            defaultValue=""
            render={({ field }) => (
              <TextField
                {...field}
                label="著者名"
                variant="outlined"
                fullWidth
              />
            )}
          />
          <Button type="submit" variant="contained" color="primary">
            検索
          </Button>
        </Stack>
      </form>
      {isLoading && (
        <div style={{ textAlign: "center", marginTop: "20px" }}>
          <CircularProgress />
        </div>
      )}
      {error && (
        <Alert severity="error" style={{ marginTop: "20px" }}>
          error
        </Alert>
      )}

      {!isLoading && results && results.data.length > 0 && (
        <List>
          {results.data.map((book, index) => (
            <ListItem key={index}>
              <Typography variant="h6">{book.title}</Typography>
              <Typography variant="body2">{book.author}</Typography>
            </ListItem>
          ))}
        </List>
      )}
    </div>
  );
};

export default BookSearchForm;

引数として渡すサーバーアクションはAPI部分をモック化しています

searchBooks.ts
"use server";

import { Book, books } from "@/const/const";

export interface SearchBooksRequest {
  title: string;
  author: string;
}

export interface SearchBooksResponse {
  data: Book[];
  status: string;
}
const searchBooks = async (
  data: SearchBooksRequest
): Promise<SearchBooksResponse> => {
  // 本来はAPIからデータを取得
  console.log(data);
  const result = books.slice(2, 4);
  //   確認用
  // await new Promise((resolve) => setTimeout(resolve, 5000));
  return {
    status: "200",
    data: result,
  };
};
export default searchBooks;

(簡略化のため型も混ざています)

const.ts
export interface Book {
  id: number;
  title: string;
  author: string;
}
export const books: Book[] = [
  {
    id: 1,
    title: "Sample Title 1",
    author: "Author 1",
  },
  {
    id: 2,
    title: "Sample Title 2",
    author: "Author 2",
  },
  {
    id: 3,
    title: "Sample Title 3",
    author: "Author 3", 
  },
  {
    id: 4,
    title: "Sample Title 4",
    author: "Author 4", 
  },
  {
    id: 5,
    title: "Sample Title 5",
    author: "Author 5",
  },
];

まとめ

読んでいただきありがとうございました。
今回はreact hook formとMUIでの検索フックスの作成を行いました。
このフックスは主に検索画面と検索結果が同じページの場合には使えそうでした。
検索画面と結果画面が分離している場合は、グローバルステートで検索フックスを管理するなどが必要そうですが、検索結果画面の表示データに関してサーバーコンポーネントで取得したいと思うので今回のフックスはつかえず、URLのクエリパラメータなど別の手段が必要そうです。

1
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
1
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?