Next.jsでお問い合わせフォーム(HyperForm)
はじめに
この記事では Next.js
で簡易的なお問い合わせフォームを作成する方法を紹介します。
バックエンドのメール送信機能については HyperForm を使用し、フロントエンド側の開発に焦点を当てたものになっています。
使用技術
その他
対象読者
-
Next.js
で簡易的なお問い合わせフォームを作る方法を知りたい方 -
React Hook Form
とValibot
の組み合わせで実装したい方 - バックエンド側のメール送信機能を作るのが面倒なのでヘッドレスフォームに任せて実装したい方
前提条件
-
Next.js
やTypeScript
やTailwind CSS
のセットアップが完了しているものとして話を進めております。 - スタイリングには
Tailwind CSS
を使用しております。 -
Node.js
のパッケージマネージャーにyarn
を使用しております。 - HTTPクライアントには
axios
を使用しております。 -
react-hot-toast
を使用し、ユーザー側に送信の成功や失敗のメッセージ表示を簡易的に済ませております。
完成イメージ
-
名前
メールアドレス
メッセージ
の送信が可能 - 各項目は必須項目でバリデーション時にはエラーメッセージを表示
- 各項目に文字数制限
-
メールアドレス
の形式のみ入力可能 -
メッセージ
は300文字以内でユーザーに入力文字数が可視化されるようにカウンター数値を設置 - 送信中なのがわかるようにスピナー表示
- 送信の完了と失敗時にトーストでメッセージ表示
準備
HyperFormの使用
- HyperFormのホームページからアカウントの作成を行います。
- 自動的に作成される カスタムURL を確認します。このURL内にユニークな フォームID が付与されています。このフォームIDを下記の形式のURLとして使用します。
https://hyperform.jp/api/async/{your-form-id}/complete
下記が公式ドキュメントの詳細です↓
- 環境変数としてセットしておきます。
NEXT_PUBLIC_HYPERFORM_URL="https://hyperform.jp/api/async/{your-form-id}/complete"
ライブラリのインストール
プロジェクトにライブラリのインストールを行います。
yarn add react-hook-form @hookform/resolvers valibot axios react-hot-toast
@hookform/resolvers とは?
@hookform/resolversは、react-hook-form
ライブラリで使用できる各種バリデーションスキーマライブラリ(例:Yup、Zod、Superstructなど)と統合を簡単に行うためのパッケージです。
※この記事では Valibot
を使用しております。
このライブラリは、指定されたスキーマに基づいてバリデーションを実行し、react-hook-form
が解釈できるエラーメッセージやバリデーションの結果を生成します
@hookform/resolvers
を使うことで、豊富なバリデーションルールを持つ外部のライブラリとreact-hook-form
を簡単に連携できます。これはコードの再利用性を高め、バリデーションロジックのメンテナンスを容易にする大きな利点です。
下記は公式ドキュメントのサンプルコードです↓
バリデーションのスキーマ定義
バリデーション用のスキーマを定義しておきます。
import * as v from "valibot"
const nameSchema = v.pipe(
v.string(),
v.minLength(1, "This field is required."),
v.maxLength(20, "Please enter no more than 20 characters.")
)
const emailSchema = v.pipe(
v.string(),
v.minLength(1, "This field is required."),
v.maxLength(255, "Please enter no more than 255 characters."),
v.email("Please enter a valid email address.")
)
const messageSchema = v.pipe(
v.string(),
v.minLength(1, "This field is required."),
v.maxLength(300, "Please enter no more than 300 characters.")
)
export const ContactSchema = v.object({
name: nameSchema,
email: emailSchema,
message: messageSchema,
})
export type ContactType = v.InferOutput<typeof ContactSchema>
react-hot-toastの設定
プロジェクトで react-hot-toast
を使用できるようにしておきます。
// --- 略 ---
+import { Toaster } from "react-hot-toast"
// --- 略 ---
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja">
<body className={inter.className}>
{children}
+ <Toaster />
</body>
</html>
)
}
Spinnerコンポーネントの作成
フォーム内容の送信が完了するまでに表示するSpinner
コンポーネントを作成しておきます。
export const Spinner = () => (
<div className=" mx-auto h-5 w-5 animate-spin rounded-full border-2 border-gray-200 border-t-transparent"></div>
)
フォーム画面の作成
最後にフォーム画面を作成します。下記がコード全体です。
このフォーム用のコンポーネントを app
ディレクトリ配下の page.tsx
で読み込んで使用します。
"use client"
import { valibotResolver } from "@hookform/resolvers/valibot"
import axios from "axios"
import { useForm } from "react-hook-form"
import { toast } from "react-hot-toast"
import { ContactSchema, ContactType } from "@/schema/contact"
import { Spinner } from "./ Spinner"
export const ContactForm = () => {
const {
register,
handleSubmit,
reset,
watch,
formState: { errors, isValid, isSubmitting },
} = useForm<ContactType>({
mode: "onBlur",
resolver: valibotResolver(ContactSchema),
})
const messageValue = watch("message", "")
const messageLength = messageValue.length
const onSubmit = handleSubmit(async (data) => {
try {
await axios.post(process.env.NEXT_PUBLIC_HYPERFORM_URL as string, data)
toast.success(
"送信が完了いたしました。\n自動返信メールをお送りしておりますのでご確認をお願いいたします。",
)
reset()
} catch (error) {
console.error("Form submit error", error)
toast.error("送信時にエラーが発生しました。\n恐れ入りますが後でもう一度お試しください。")
}
})
return (
<form
method="post"
onSubmit={onSubmit}
className="flex w-[300px] flex-col items-center justify-center gap-3"
>
<div className="w-full">
<label htmlFor="name" className="text-sm text-gray-600">
お名前
</label>
<input
type="text"
id="name"
{...register("name")}
placeholder="山田 太郎"
className="w-full border p-3 shadow hover:border-gray-400"
/>
{errors.name && (
<span className="self-start text-xs text-red-500">{errors.name.message}</span>
)}
</div>
<div className="w-full">
<label htmlFor="email" className="text-sm text-gray-600">
メールアドレス
</label>
<input
type="text"
id="email"
{...register("email")}
placeholder="mail@example.com"
className="w-full border p-3 shadow hover:border-gray-400"
/>
{errors.email && (
<span className="self-start text-xs text-red-500">{errors.email.message}</span>
)}
</div>
<div className="w-full">
<label htmlFor="message" className="text-sm text-gray-600">
メッセージ
</label>
<textarea
id="message"
{...register("message")}
placeholder="お問い合わせ内容を入力してください"
rows={6}
className="w-full border p-3 shadow hover:border-gray-400"
></textarea>
<div className="pr-1 text-right text-xs text-gray-400">{messageLength}/300</div>
{errors.message && (
<span className="self-start text-xs text-red-500">{errors.message.message}</span>
)}
</div>
<button
type="submit"
disabled={!isValid || isSubmitting}
className={`w-full rounded bg-lime-600 p-3 text-white transition ${
!isValid || isSubmitting ? "cursor-not-allowed opacity-60" : "hover:bg-lime-700"
}`}
>
{isSubmitting ? <Spinner /> : "送信"}
</button>
</form>
)
}
use client の宣言について
react-hook-form
で フォームの状態管理を行うので、 Server Component ではなく Client Component として使用します。Client Component として使用する場合は先頭にuse client
の宣言が必要です。
useForm について
useForm
まず、useForm
は、react-hook-form
ライブラリの主要なフックです。このフックを使って、フォームの状態やメソッドを管理・制御します。
const {
register,
handleSubmit,
reset,
watch,
formState: { errors: formatError, isValid, isSubmitting },
} = useForm<ContactType>({
mode: "onBlur",
resolver: valibotResolver(ContactSchema),
});
上記のコードでは、useForm
からいくつかの機能や状態を分割代入して取得しています。
register
register
は、フォームの各インプット要素に関連付ける関数です。これを使うことで、そのインプット要素の値や状態を react-hook-form
で監視・管理することができるようになります。
例:
<input {...register("name")} />
handleSubmit
handleSubmit
は、フォームの送信時に呼び出される関数をラップする関数です。これを利用することで、フォームが正しくバリデーションされた後に、指定したコールバック関数(上記コードでは onSubmit
)が呼び出されます。
reset
reset
は、フォームの全てのフィールドを初期状態に戻す関数です。上記のコードでは、フォームの送信に成功した後にこれを使用して、フォームのフィールドをクリアしています。
watch
watch
は、指定したフォームフィールドの値をリアルタイムで監視する関数です。
第一引数には監視したいフィールドの名前(string)を指定します。
第二引数にはそのフィールドのデフォルト値を指定します。
const messageValue = watch("message", "");
上記のコードでは、"message" フィールドの値を監視しており、その値が messageValue
に格納されます。今回はデフォルト値は不要なため空文字にしています。
messageLength
messageLength
は、messageValue
の文字数を取得するための変数です。これを使用することで、テキストエリアの文字数をリアルタイムでユーザーに表示することができます。
formState
formState
は、フォーム全体の状態を含むオブジェクトです。この中から、エラー情報 (errors
)、フォームが有効かどうかの状態 (isValid
)、及びフォームが送信中かどうかの状態 (isSubmitting
) を取得しています。
-
errors
: バリデーションエラー情報を含むオブジェクトです。 -
isValid
: フォームがバリデーションに通っているかどうかのブール値です。 -
isSubmitting
: フォームが送信中かどうかのブール値です。
最後に、useForm
のオプションとして、mode: "onBlur"
と resolver: valibotResolver(ContactSchema)
を指定しています。
-
mode: "onBlur"
: このモードは、ユーザーがインプットフィールドからフォーカスを外した時にバリデーションをトリガーすることを指定します。 -
resolver
: これは、バリデーションロジックを提供する関数を指定するためのオプションです。ここではValibot
で定義したバリデーションスキーマを指定したいので 、valibotResolver
とContactSchema
を使って、フォームのバリデーションを行っています。
onSubmit関数 について
onSubmit関数の主な役割は、フォームデータをサーバーに送信する処理と、その結果に応じて適切なフィードバックをユーザーに提供することです。
const onSubmit = handleSubmit(async (data) => {
try {
await axios.post(process.env.NEXT_PUBLIC_HYPERFORM_URL as string, data)
toast.success(
"送信が完了いたしました。\n自動返信メールをお送りしておりますのでご確認をお願いいたします。",
)
reset()
} catch (error) {
console.error("Form submit error", error)
toast.error("送信時にエラーが発生しました。\n恐れ入りますが後でもう一度お試しください。")
}
})
詳細な解説:
-
const onSubmit = handleSubmit(async (data) => {...}
:-
onSubmit
という名前の関数を定義しています。 -
handleSubmit
はreact-hook-form
の関数で、この関数を通じて提供されたコールバック関数(この場合は非同期関数async (data) => {...}
)を呼び出します。data
引数はフォーム内のすべての入力値を保持するオブジェクトです。
-
-
try {...}
:- エラーが発生する可能性のあるコードを
try
ブロック内に配置します。もしエラーが発生すれば、直ちに対応するcatch
ブロックに制御が移ります。
- エラーが発生する可能性のあるコードを
-
await axios.post(process.env.NEXT_PUBLIC_HYPERFORM_URL as string, data)
:-
axios.post
を使用して、データをサーバーに非同期的にPOST送信しています。 - 送信先のURLは環境変数
NEXT_PUBLIC_HYPERFORM_URL
から取得しています。as string
はTypeScriptの型アサーションで、この変数を文字列として扱うことを明示しています。
-
-
toast.success(...)
:- フォームのデータが正常に送信された場合、ユーザーに成功メッセージを表示します。これは
react-hot-toast
のtoast
関数を使用しています。
- フォームのデータが正常に送信された場合、ユーザーに成功メッセージを表示します。これは
-
reset()
:- フォームデータの送信が成功した後、
reset
関数を使用してフォームの入力を初期状態に戻します。
- フォームデータの送信が成功した後、
-
catch (error) {...}
:- 何らかのエラーが発生した場合(例: ネットワーク接続の問題、サーバーエラーなど)、
catch
ブロックが実行されます。
- 何らかのエラーが発生した場合(例: ネットワーク接続の問題、サーバーエラーなど)、
-
console.error("Form submit error", error)
:- 発生したエラーをコンソールに出力します。
-
toast.error(...)
:- 発生したエラーに関する情報をユーザーに表示するために使用されます。
繰り返しになりますが、この関数全体の目的は、フォームデータをサーバーに送信し、その結果に基づいてユーザーに適切なフィードバックを提供することです。
return以降 について
Tailwind CSS
でのスタイリングは適当です。
return (
<form
method="post"
onSubmit={onSubmit}
className="flex w-[300px] flex-col items-center justify-center gap-3"
>
<div className="w-full">
...
</div>
...
<button
...
>
{isSubmitting ? <Spinner /> : "送信"}
</button>
</form>
)
-
<form>タグ:
-
method="post"
は、このフォームのデータ送信方法がPOSTであることを示しています。 -
onSubmit={onSubmit}
は、フォームが送信されるときにonSubmit
関数が実行されることを指定しています。
-
-
<input>タグ:
-
{...register("name")}
や{...register("email")}
はreact-hook-form
を使用して、このinputフィールドをフォームと関連付けるための構文です。
-
-
<textarea>タグ:
- こちらも
{...register("message")}
を使用して、react-hook-form
との連携を確立しています。
- こちらも
-
<div className="...">{messageLength}/300</div>:
- メッセージの入力文字数とその最大値(この場合は300)を表示するためのdiv要素です。
-
<span className="...">{errors.name.message}</span>:
- エラーメッセージを表示するためのspanタグです。
-
errors.name.message
は、name
フィールドのエラーメッセージを取得します。 -
errors.email.message
は、email
フィールドのエラーメッセージを取得します。 -
errors.message.message
は、message
フィールドのエラーメッセージを取得します。
-
<button>タグ:
-
{isSubmitting ? <Spinner /> : "送信"}
は、フォームの送信が進行中であるかどうかに基づいて、スピナーアイコンまたは"送信"というテキストを表示します。
-
このjsx(tsx)
の構造は、シンプルなコンタクトフォームを提供するためのものです。各入力フィールドには対応するラベルがあり、ユーザーが入力したデータはonSubmit
関数を通じてサーバーに送信されます。
まとめ
-
Next.js
で、React Hook Form
とValibot
の組み合わせでお問い合わせフォームを実装しました。 - ヘッドレスフォーム に HyperForm を使用し、メール送信機能を実現しました。
HyperForm
に関しては開発者の方の下記記事が参考になります。今回は HyperForm
の機能について説明は省いていますが、お問い合わせフォームに必要とされる色々な機能を実現できそうなので気になる方はご参照ください。
Discussion