😸

react-hook-formで問い合わせページ作成

2023/07/27に公開

はじめに

  • 問い合わせフォームを作成します。
  • react-hook-formstate管理します。
  • Zodバリデーションをします。
  • TailwindCSS見た目を作ります。

以下が作業用のリポジトリです。

https://github.com/hayato94087/nextjs-contact-sample

react-hook-formとは

  • React でフォームを作成するためのライブラリです。
  • フォームの状態を管理します。従来だと、入力値毎に useState を使って管理する必要がありましたが、簡略化できます。
  • 入力値のバリデーションを行うこともできます。Zod を利用したほうが良いです。

https://www.react-hook-form.com/

Zodとは

  • TypeScript の型システムを活用し、データの形状や構造を静的にチェックできるライブラリです。
  • ユーザの入力や API のレスポンスなど、アプリケーションで取り扱いデータが期待する型であることを保証できます。

https://zod.dev/

実施内容

それでは、実際に問い合わせフォームを作成していきます。

Next.jsプロジェクトの新規作成

作業するプロジェクトを新規に作成していきます。

長いので、折りたたんでおきます。

新規プロジェクト作成と初期環境構築の手順詳細
$ pnpm create next-app@latest nextjs-contact-sample --typescript --eslint --import-alias "@/*" --src-dir --use-pnpm --tailwind --app
$ cd nextjs-contact-sample

以下の通り不要な設定を削除し、プロジェクトの初期環境を構築します。

$ mkdir src/styles
$ mv src/app/globals.css src/styles/globals.css
src/styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
src/app/page.tsx
export default function Home() {
  return (
    <main className="text-lg">
      テストページ
    </main>
  )
}
src/app/layout.tsx
import '@/styles/globals.css'

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body className="">{children}</body>
    </html>
  );
}
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  plugins: [],
};
tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
+    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

コミットします。

$ pnpm build
$ git add .
$ git commit -m "新規にプロジェクトを作成し, 作業環境を構築"

react-hook-form, zod, @hookform/resolversをインストール

react-hook-form, zod をインストールします。

terminal
$ pnpm add react-hook-form
$ pnpm add zod
$ pnpm add @hookform/resolvers

コミットします。

$ pnpm build
$ git add .
$ git commit -m "react-hook-form, zod, @hookform/resolversをインストール"

Schemaを作成

zod で schema を作成します。

$ mkdir src/schema
$ touch src/schema/contact.ts
src/app/contact/schema.ts
import { z } from "zod";

const email: z.ZodString = z
  .string({ required_error: "入力が必須の項目です" })
  .min(1, { message: "入力が必須の項目です" })
  .max(255, { message: "255文字以内で入力してください" })
  .email({ message: "メールアドレスの形式で入力してください" });
const telephone: z.ZodString = z
  .string({ required_error: "入力が必須の項目です" })
  .min(10, { message: "電話番号を入力してください" })
  .max(14, { message: "入力値が長すぎます" });
const givenName: z.ZodString = z
  .string({ required_error: "入力が必須の項目です" })
  .min(1, { message: "入力が必須の項目です" })
  .max(20, { message: "入力値が長すぎます" });
const lastName: z.ZodString = z
  .string({ required_error: "入力が必須の項目です" })
  .min(1, { message: "入力が必須の項目です" })
  .max(20, { message: "入力値が長すぎます" });
const organizationName: z.ZodString = z
  .string({ required_error: "入力が必須の項目です" })
  .min(1, { message: "入力が必須の項目です" })
  .max(50, { message: "入力値が長すぎます" });
const message: z.ZodString = z
  .string({ required_error: "入力が必須の項目です" })
  .min(1, { message: "入力が必須の項目です" })
  .max(4098, { message: "入力値が長すぎます" });
const agree: z.ZodLiteral<string> = z.literal("true", {
  errorMap: () => ({ message: "同意が必須です" }),
});

export const ContactSchema = z.object({
  email: email,
  telephone: telephone,
  givenName: givenName,
  lastName: lastName,
  organizationName: organizationName,
  message: message,
  agree: agree,
});
export type ContactType = z.infer<typeof ContactSchema>;

フォームコンポーネントを作成

問い合わせフォームのコンポーネントを作成します。react-hook-form で state 管理、Zod でバリデーション、TailwindCSS で見た目を作成します。

  • 入力値のバリデーションを Zod で実行しています。
  • 入力値が正しくないときは、エラーメッセージが画面に表示されます。
  • 入力値が正しくないときは、送信ボタンをクリックできません。
  • 送信ボタンをクリックすると、クライアントのコンソールに入力値が表示されます。
$ mkdir src/components
$ touch src/components/contact-form.tsx
src/components/contact-form.tsx
"use client";

import { FC } from "react";
import { ContactSchema, ContactType } from "@/schema/contact";
import { zodResolver } from "@hookform/resolvers/zod";
import { SubmitHandler, useForm } from "react-hook-form";

interface ContactFormProps {}

const ContactForm: FC<ContactFormProps> = ({}) => {
  const handleOnSubmit: SubmitHandler<ContactType> = (data) => {
    console.log(data);
  };

  const {
    register,
    handleSubmit,
    formState: { errors: formatError, isValid, isSubmitting },
  } = useForm<ContactType>({
    mode: "onBlur",
    resolver: zodResolver(ContactSchema),
  });

  return (
    <form
      method="post"
      onSubmit={(event) => {
        void handleSubmit(handleOnSubmit)(event);
      }}
      className="flex flex-col space-y-10"
    >
      <label className="flex flex-col space-y-1">
        <div className="text-sm font-bold mb-1">メールアドレス</div>
        <input
          type="text"
          {...register("email")}
          className="text-gray-800 mt-4 rounded-md border py-2 px-3"
          placeholder="例)mail@example.com"
        />
        {formatError.email && (
          <div className="text-red-500 pl-1 pt-1 text-xs">
            {formatError.email.message}
          </div>
        )}
      </label>

      <label className="flex flex-col space-y-1">
        <div className="text-sm font-bold mb-1">電話番号</div>
        <input
          type="text"
          {...register("telephone")}
          className="text-gray-800 mt-4 rounded-md border py-2 px-3"
          placeholder="例)09012345678"
        />
        {formatError.telephone && (
          <div className="text-red-500 pl-1 pt-1 text-xs">
            {formatError.telephone.message}
          </div>
        )}
      </label>

      <label className="flex flex-col space-y-1">
        <div className="text-sm font-bold mb-1">お名前</div>
        <input
          type="text"
          {...register("lastName")}
          className="text-gray-800 mt-4 rounded-md border py-2 px-3"
          placeholder="例)山田"
        />
        {formatError.lastName && (
          <div className="text-red-500 pl-1 pt-1 text-xs">
            {formatError.lastName.message}
          </div>
        )}
        <input
          type="text"
          {...register("givenName")}
          className="text-gray-800 mt-4 rounded-md border py-2 px-3"
          placeholder="例)太郎"
        />
        {formatError.givenName && (
          <div className="text-red-500 pl-1 pt-1 text-xs">
            {formatError.givenName.message}
          </div>
        )}
      </label>

      <label className="flex flex-col space-y-1">
        <div className="text-sm font-bold mb-1">企業名</div>
        <input
          type="text"
          {...register("organizationName")}
          className="text-gray-800 mt-4 rounded-md border py-2 px-3"
          placeholder="例)株式会社◯✕△"
        />
        {formatError.organizationName && (
          <div className="text-red-500 pl-1 pt-1 text-xs">
            {formatError.organizationName.message}
          </div>
        )}
      </label>

      <label className="flex flex-col space-y-1">
        <div className="text-sm font-bold mb-1">お問い合わせ内容</div>
        <textarea
          {...register("message")}
          className="h-36 border px-2 py-1"
        ></textarea>

        {formatError.message && (
          <div className="text-red-500 pl-1 pt-1 text-xs">
            {formatError.message.message}
          </div>
        )}
      </label>

      <div className="flex flex-col items-center space-y-1">
        <div className="flex flex-row items-center space-x-2">
          <label className="flex flex-row items-center space-x-2">
            <input
              type="checkbox"
              value="true"
              {...register("agree")}
              className="h-5 w-5"
            />
            <p>個人情報取り扱いに同意する</p>
          </label>
        </div>
        {formatError.agree && (
          <div className="text-red-500 pl-1 pt-1 text-center text-xs">
            {formatError.agree.message}
          </div>
        )}
      </div>
      <button
        type="submit"
        disabled={!isValid || isSubmitting}
        className="bg-slate-800 hover:bg-slate-600 rounded px-4 py-2 text-white  disabled:bg-gray-300 md:self-center"
      >
        送信する
      </button>
    </form>
  );
};

export default ContactForm;

ページを修正

下記の通り page.tsx を上書きします。

src/app/page.tsx
import ContactForm from "@/components/contact-form";

export default function Home() {
  return (
    <main className="container relative mx-auto">
      <div className="mx-auto lg:w-[800px] py-10 px-10">
        <h1 className="text-2xl font-bold text-center pb-10">お問い合わせ</h1>
        <ContactForm />
      </div>
    </main>
  );
}

動作確認

ローカルサーバで動作確認をします。

$ pnpm dev

バリデーションが正しく動作していることが確認できます。適切に入力した後、送信ボタンをクリックすると、コンソールに入力値が表示されます。

コミットします。

$ pnpm build
$ git add .
$ git commit -m "問い合わせフォームを作成"

まとめ

  • 問い合わせフォームを作成しました。
    • react-hook-formstate管理
    • Zodバリデーション
    • TailwindCSS見た目を作成。
  • 以下の機能を実装しました。
    • 入力値のバリデーションを Zod で実行。
    • 入力値が正しくないときは、エラーメッセージを画面に表示。
    • 入力値が正しくないときは、送信ボタンのクリックを不可。
    • 送信ボタンをクリックすると、クライアントのコンソールに入力値を表示。

以下が作業用のリポジトリです。

https://github.com/hayato94087/nextjs-contact-sample

参考

https://www.react-hook-form.com/
https://zod.dev/
https://github.com/react-hook-form/resolvers

Discussion