😸
react-hook-formで問い合わせページ作成
はじめに
- 問い合わせフォームを作成します。
- react-hook-form でstate管理します。
- Zod でバリデーションをします。
- TailwindCSS で見た目を作ります。
以下が作業用のリポジトリです。
react-hook-formとは
- React でフォームを作成するためのライブラリです。
- フォームの状態を管理します。従来だと、入力値毎に useState を使って管理する必要がありましたが、簡略化できます。
- 入力値のバリデーションを行うこともできます。Zod を利用したほうが良いです。
Zodとは
- TypeScript の型システムを活用し、データの形状や構造を静的にチェックできるライブラリです。
- ユーザの入力や API のレスポンスなど、アプリケーションで取り扱いデータが期待する型であることを保証できます。
実施内容
それでは、実際に問い合わせフォームを作成していきます。
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-form でstate管理。
- Zod でバリデーション。
- TailwindCSS で見た目を作成。
- 以下の機能を実装しました。
- 入力値のバリデーションを Zod で実行。
- 入力値が正しくないときは、エラーメッセージを画面に表示。
- 入力値が正しくないときは、送信ボタンのクリックを不可。
- 送信ボタンをクリックすると、クライアントのコンソールに入力値を表示。
以下が作業用のリポジトリです。
参考
Discussion