お問い合わせフォームの作成記録
送信処理や送信ボタン状態管理
SendGrid API を使用しています。
日本の代理店で登録しようとすると、審査が厳しくなって個人利用不可になってしまったので、Twillo SendGridから登録します。
useActionStateへの更新版
- Next.js - 14.3.0-canary.28
- react - 18.3.1
送信中と送信成功後に、ボタンを無効化する処理を追加
page.tsx
'use client';
import { useForm, FormProvider } from "react-hook-form"
import { StringConstaraint } from './schema';
import { ContactFormType } from './types';
import { zodResolver } from '@hookform/resolvers/zod';
import { useState } from 'react';
import Form from './form';
import Confirm from './confirm';
export default function App() {
// フォームエラーの制御
const methods = useForm<ContactFormType>({
mode: "onBlur",
resolver: zodResolver(StringConstaraint)
})
// モダールウインドウの制御
const [isOpen, setIsOpen] = useState(false);
return (
<>
<main className="min-h-screen bg-gray-200 w-full p-8">
<div className="max-w-xl">
<FormProvider {...methods}>
<Form onOk={() => setIsOpen(true)}/>
<Confirm open={isOpen} onCancel={() => setIsOpen(false)}/>
</FormProvider>
</div>
</main>
</>
)
}
form.tsx
'use client';
import { useFormContext } from "react-hook-form";
import { ContactFormType } from './types';
type Props = {
onOk: () => void;
}
export default function Form({ onOk }: Props) {
const methods = useFormContext<ContactFormType>();
const {
register,
handleSubmit,
formState: {errors}
} = methods;
return(
<>
<form onSubmit={ handleSubmit(onOk) } className="bg-white shadow-md rounded p-8">
<h3 className="block text-gray-700 text-lg font-bold mb-2">お問い合わせ</h3>
<div className="mb-4">
<label htmlFor="名前" className="block text-gray-700 text-sm font-bold mb-2">お名前</label>
<input {...register("name")} type="text" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
<p className="text-red-500 text-xs italic mb-4">{ errors.name?.message }</p>
<label htmlFor="メール" className="block text-gray-700 text-sm font-bold mb-2">メールアドレス</label>
<input {...register("email")} type="email" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
<p className="text-red-500 text-xs italic mb-4">{ errors.email?.message }</p>
<label htmlFor="お問い合わせ" className="block text-gray-700 text-sm font-bold mb-2">お問い合わせ内容</label>
<textarea {...register("message")} rows={8} className="resize-none shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
<p className="text-red-500 text-xs italic mb-4">{ errors.message?.message }</p>
</div>
<div className="flex items-center justify-center">
<button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-8 rounded focus:outline-none focus:shadow-outline">
確認
</button>
</div>
</form>
</>
)
}
confirm.tsx
'use client';
import { useFormContext } from "react-hook-form";
import { ContactFormType } from './types';
import { SendMessageAction } from "./action";
import { useActionState } from "react";
type Props = {
open: boolean;
onCancel: () => void;
}
export default function Confirm({onCancel, open}:Props) {
const methods = useFormContext<ContactFormType>();
const {
getValues,
handleSubmit,
} = methods;
const [formState, sendAction, pending] = useActionState(SendMessageAction, {"success": false, "message" : ""});
return open ?(
<>
<form onSubmit={handleSubmit(sendAction)} className="bg-white top-1/4 left-1/2 transform -translate-x-1/2 w-1/4 p-5 flex flex-col items-start absolute z-20">
<h2 className="text-xl font-bold mb-5">送信内容の確認</h2>
<label className="text-gray-700 text-sm font-bold mb-2">お名前</label>
<p className="text-lg mb-5">{ getValues('name') }</p>
<label className="text-gray-700 text-sm font-bold mb-2">メールアドレス</label>
<p className="text-lg mb-5 w-full overflow-hidden">{ getValues('email') }</p>
<label className="text-gray-700 text-sm font-bold mb-2">お問い合わせ内容</label>
<p className="text-lg mb-5 break-all max-h-60 overflow-y-auto">{ getValues('message') }</p>
<div className="flex mt-auto w-full">
<button type="submit" disabled={pending || formState.success} className={`px-8 py-2 rounded text-white ${formState.success ? "bg-gray-500 cursor-not-allowed" : "bg-blue-600 hover:bg-blue-700"} px-8 py-2 mx-auto`}>
{pending ? "送信中..." : "送信"}
</button>
<button type="button" onClick={ onCancel } className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
キャンセル
</button>
</div>
<div className={`flex justify-center mt-4 ${formState.success ? '' : 'text-red-600'}`}>{formState.message}</div>
</form>
<div onClick={ onCancel } className="fixed bg-black bg-opacity-50 w-full h-full top-0 left-0 z-10"/>
</>
):(<></>);
};
action.ts
'use server';
//source sendgrid.env
import { ContactFormType } from './types';
type State = {
success?: boolean;
message?: string;
};
export async function SendMessageAction(prevState: State,params: ContactFormType) {
const formData = new FormData()
Object.entries(params).forEach(([key, value]) => {
formData.append(key, value)
})
const headers = new Headers([
['Content-Type', 'application/json'],
["Authorization", "Bearer " + process.env.SENDGRID_API_KEY]
])
const requestBody = {
"personalizations": [
{
"to": [
{
"email": formData.get("email")
}
]
}
],
"subject": "お問い合わせを受け付けました。",
"from": {
"email": "Your@email.com"
},
"content": [
{
"type": "text/plain",
"value": formData.get("name") + "様\r\n\r\n以下の内容でお問い合わせを受け付けました。\r\n------\r\n" + formData.get("message")
}
]
};
try {
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: headers,
body: JSON.stringify(requestBody)
});
if(response.ok) {
console.log('Email sent')
return {
success: true,
message: "お問い合わせを受け付けました。"
};
}else {
console.error(response);
return {
success: false,
message: "お問い合わせの送信に失敗しました。"
};
}
} catch (error) {
console.error(error);
return {
success: false,
message: "サーバーが応答しません。しばらくしてから再度お試しください。"
};
}
}
参考
SendGridのAPI KEY発行手続きと、クイックスタートガイド
[6/14 追加] Next.js 14.2.4 useStateActionを使わない方法
export default function Confirm({onCancel, open}:Props) {
const methods = useFormContext<FormType>();
const { getValues, handleSubmit } = methods;
- const [formState, sendAction, pending] = useActionState(SendMessageAction, {"success": false, "message" : ""});
+ const initialState:State = {};
+ const [formState, sendAction] = useFormState(SendMessageAction, initialState);
+ const { pending } = useFormStatus();
initialState
は初期値設定用です。action.ts のStateの型定義と同一のものを拾って来てください
TODO
- React Hook Form を使ってお問い合せフォームの雛形を作成
- 入力にエラーが発生している場合、送信ボタンを押せないようにする モーダルウインドウで発生するみたい...
- RHFから、データを受取してモーダルウインドウに表示する
- FIX: モーダルウインドウのCSSがおかしいので要修正
- 送信処理(旧データのaction.tsを再利用)
- よく使う型情報は、types.ts を作ってそちらに集約
今後の改修
- 確認ボタンを押すと送信ボタンと、入力内容の確認をする表示がでるモーダルウインドウを実装する
- モーダルウインドウの表示
- RHFから、データを受取してモーダルウインドウに表示する
- 送信ボタン/閉じるボタン/画面外を押すと閉じる を実装する
- 送信処理(旧データのaction.tsを再利用)
- action.ts にデータを送信すると送受信結果が返ってくるので、結果をモーダルウインドウに表示する。
- 送信用のアクションを呼び出す方法について...
- 送信ボタン:送信中 -> 送信結果:送信しました, 送信エラー -> OK:完了 NG:閉じる
お問い合わせフォームの雛形
Next.js + Tailwind で作成
フォームの作成には、React Hook Form
'use client';
import { StringConstaraint } from './schema'
import { useForm } from "react-hook-form"
import { zodResolver } from '@hookform/resolvers/zod';
interface ContactFormType {
name: string;
email: string;
text: string;
}
export default function Form() {
const {
register,
handleSubmit,
formState: {errors}
} = useForm<ContactFormType>({
mode: "onBlur",
resolver: zodResolver(StringConstaraint)
});
const onSubmit = (data :ContactFormType) => {
console.log(data);
};
return(
<div className="max-w-xl">
<form onSubmit={handleSubmit(onSubmit)} className="bg-white shadow-md rounded p-8">
<h3 className="block text-gray-700 text-lg font-bold mb-2">お問い合わせ</h3>
<div className="mb-4">
<label htmlFor="名前" className="block text-gray-700 text-sm font-bold mb-2">お名前</label>
<input {...register("name")} type="text" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
<p className="text-red-500 text-xs italic mb-4">{errors.name?.message}</p>
<label htmlFor="メール" className="block text-gray-700 text-sm font-bold mb-2">メールアドレス</label>
<input {...register("email")} type="email" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
<p className="text-red-500 text-xs italic mb-4">{errors.email?.message}</p>
<label htmlFor="お問い合わせ" className="block text-gray-700 text-sm font-bold mb-2">お問い合わせ内容</label>
<textarea {...register("text")} rows={8} className="resize-none shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
<p className="text-red-500 text-xs italic mb-4">{errors.text?.message}</p>
</div>
<div className="flex items-center justify-center">
<button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-8 rounded focus:outline-none focus:shadow-outline">
送信
</button>
</div>
<div className="flex items-center justify-center text-red-500 text-xs italic mt-2"></div>
</form>
</div>
);
}
スキーマの作成
スキーマ宣言ライブラリには、Zod を使用
import { z } from 'zod';
export const StringConstaraint = z.object({
name: z.string()
.min(1, { message: '入力が必須の項目です' })
.max(20, { message: '20文字以内で入力してください' })
.regex(/^[^<>"'\\/]*$/, { message: '特殊文字は使用できません' }),
email: z.string()
.min(1, { message: '入力が必須の項目です' })
.max(255, { message: '255文字以内で入力してください' })
.email({ message: 'メールアドレスの形式で入力してください' }),
text: z.string()
.min(10, { message: '入力が必須の項目です' })
.max(1000, { message: '1000文字以内で入力してください' })
.regex(/^[^<>"'\\/]*$/, { message: '特殊文字は使用できません' }),
});
button type ="sbummit"
を押下した際に、モーダルウインドウを表示するように修正
'use client';
import { StringConstaraint } from './schema'
import { useForm } from "react-hook-form"
import { zodResolver } from '@hookform/resolvers/zod';
import { useState } from 'react';
type ContactFormType = {
name: string;
email: string;
text: string;
}
type ModalProps = {
open: boolean;
onCancel: () => void;
onOk: () => void;
};
const Modal = (props: ModalProps) => {
return props.open ? (
<>
<div className="bg-white top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-80 h-48 p-5 flex flex-col items-start absolute z-20">
<h1 className="text-xl font-bold mb-5">Title</h1>
<p className="text-lg mb-5">Dialog Message.</p>
<div className="flex mt-auto w-full">
<button onClick={() => props.onOk()} className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
送信
</button>
<button onClick={() => props.onCancel()} className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
キャンセル
</button>
</div>
</div>
<div
className="fixed bg-black bg-opacity-50 w-full h-full top-o left-0 z-10"
onClick={() => props.onCancel()}
/>
</>
) : (<></>);
};
export default function Form() {
const [isOpen, setIsOpen] = useState(false);
const {
register,
handleSubmit,
formState: {errors}
} = useForm<ContactFormType>({
mode: "onBlur",
resolver: zodResolver(StringConstaraint)
});
const onSubmit = (data :ContactFormType) => {
console.log(data);
setIsOpen(true);
};
return(
<>
<Modal
open={isOpen}
onCancel={() => setIsOpen(false)}
onOk={() => setIsOpen(false)}
/>
<div className="max-w-xl">
<form onSubmit={handleSubmit(onSubmit)} className="bg-white shadow-md rounded p-8">
<h3 className="block text-gray-700 text-lg font-bold mb-2">お問い合わせ</h3>
<div className="mb-4">
<label htmlFor="名前" className="block text-gray-700 text-sm font-bold mb-2">お名前</label>
<input {...register("name")} type="text" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
<p className="text-red-500 text-xs italic mb-4">{errors.name?.message}</p>
<label htmlFor="メール" className="block text-gray-700 text-sm font-bold mb-2">メールアドレス</label>
<input {...register("email")} type="email" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
<p className="text-red-500 text-xs italic mb-4">{errors.email?.message}</p>
<label htmlFor="お問い合わせ" className="block text-gray-700 text-sm font-bold mb-2">お問い合わせ内容</label>
<textarea {...register("text")} rows={8} className="resize-none shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
<p className="text-red-500 text-xs italic mb-4">{errors.text?.message}</p>
</div>
<div className="flex items-center justify-center">
<button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-8 rounded focus:outline-none focus:shadow-outline">
確認
</button>
</div>
<div className="flex items-center justify-center text-red-500 text-xs italic mt-2"></div>
</form>
</div>
</>
);
}
form
のデータをuseStateに載せて、モーダルウインドウへの表示。
'use client';
import { StringConstaraint } from './schema'
import { useForm } from "react-hook-form"
import { zodResolver } from '@hookform/resolvers/zod';
import { useState } from 'react';
type ContactFormType = {
name: string;
email: string;
text: string;
}
type ModalProps = {
open: boolean;
+ propForm: ContactFormType;
onCancel: () => void;
onOk: () => void;
};
const Modal = (props: ModalProps) => {
return props.open ? (
<>
<div className="bg-white top-1/4 left-1/2 transform -translate-x-1/2 w-1/4 p-5 flex flex-col items-start absolute z-20">
<h2 className="text-xl font-bold mb-5">送信内容の確認</h2>
+ <label className="text-gray-700 text-sm font-bold mb-2">お名前</label>
+ <p className="text-lg mb-5">{props.propForm?.name}</p>
+ <label className="text-gray-700 text-sm font-bold mb-2">メールアドレス</label>
+ <p className="text-lg mb-5 w-full overflow-hidden">{props.propForm?.email}</p>
+ <label className="text-gray-700 text-sm font-bold mb-2">お問い合わせ内容</label>
+ <p className="text-lg mb-5 break-all max-h-60 overflow-y-auto">{props.propForm?.text}</p>
<div className="flex mt-auto w-full">
<button onClick={() => props.onOk()} className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
送信
</button>
<button onClick={() => props.onCancel()} className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
キャンセル
</button>
</div>
</div>
{/*モーダルウインドウ以外をクリックした場合の処理*/}
<div onClick={() => props.onCancel()} className="fixed bg-black bg-opacity-50 w-full h-full top-0 left-0 z-10"/>
</>
) : (<></>);
};
export default function Form() {
+ const [formProps, setFormProps] = useState<ContactFormType>({name: '', email: '', text: ''});
const [isOpen, setIsOpen] = useState(false);
const {
register,
handleSubmit,
formState: {errors}
} = useForm<ContactFormType>({
mode: "onBlur",
resolver: zodResolver(StringConstaraint)
});
const onSubmit = (formProps :ContactFormType) => {
console.log(formProps);
+ setFormProps(formProps);
setIsOpen(true);
};
return(
<>
<Modal
open={isOpen}
+ propForm={formProps}
onCancel={() => setIsOpen(false)}
onOk={() => setIsOpen(false)}
/>
<div className="max-w-xl">
<form onSubmit={handleSubmit(onSubmit)} className="bg-white shadow-md rounded p-8">
<h3 className="block text-gray-700 text-lg font-bold mb-2">お問い合わせ</h3>
<div className="mb-4">
<label htmlFor="名前" className="block text-gray-700 text-sm font-bold mb-2">お名前</label>
<input {...register("name")} type="text" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
<p className="text-red-500 text-xs italic mb-4">{errors.name?.message}</p>
<label htmlFor="メール" className="block text-gray-700 text-sm font-bold mb-2">メールアドレス</label>
<input {...register("email")} type="email" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
<p className="text-red-500 text-xs italic mb-4">{errors.email?.message}</p>
<label htmlFor="お問い合わせ" className="block text-gray-700 text-sm font-bold mb-2">お問い合わせ内容</label>
<textarea {...register("text")} rows={8} className="resize-none shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
<p className="text-red-500 text-xs italic mb-4">{errors.text?.message}</p>
</div>
<div className="flex items-center justify-center">
<button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-8 rounded focus:outline-none focus:shadow-outline">
確認
</button>
</div>
<div className="flex items-center justify-center text-red-500 text-xs italic mt-2"></div>
</form>
</div>
</>
);
}
コンポーネント毎に分割する
FormProviderを使って、複数コンポーネントでformデータの共有
'use client';
import { useForm, FormProvider } from "react-hook-form"
import { StringConstaraint } from '../schema'
import { zodResolver } from '@hookform/resolvers/zod';
import { useState } from 'react';
import Form from './form';
import Confirm from './confirm';
type ContactFormType = {
name: string;
email: string;
text: string;
}
export default function App() {
// フォームエラーの制御
const methods = useForm<ContactFormType>({
mode: "onBlur",
resolver: zodResolver(StringConstaraint)
})
// モダールウインドウの制御
const [isOpen, setIsOpen] = useState(false);
return (
<>
<main className="min-h-screen bg-gray-200 w-full p-8">
<div className="max-w-xl">
<FormProvider {...methods}>
<form className="bg-white shadow-md rounded p-8">
<Form onOk={() => setIsOpen(true)}/>
<Confirm open={isOpen} onCancel={() => setIsOpen(false)} onOk={() => (setIsOpen(false))}/>
</form>
</FormProvider>
</div>
</main>
</>
)
}
'use client';
import { useFormContext } from "react-hook-form";
type ContactFormType = {
name: string;
email: string;
text: string;
}
type Props = {
onOk: () => void
}
export default function Form({ onOk }: Props) {
const methods = useFormContext<ContactFormType>();
const {
register,
formState: {errors}
} = methods;
return(
<>
<h3 className="block text-gray-700 text-lg font-bold mb-2">お問い合わせ</h3>
<div className="mb-4">
<label htmlFor="名前" className="block text-gray-700 text-sm font-bold mb-2">お名前</label>
<input {...register("name")} type="text" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
<p className="text-red-500 text-xs italic mb-4">{errors.name?.message}</p>
<label htmlFor="メール" className="block text-gray-700 text-sm font-bold mb-2">メールアドレス</label>
<input {...register("email")} type="email" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
<p className="text-red-500 text-xs italic mb-4">{errors.email?.message}</p>
<label htmlFor="お問い合わせ" className="block text-gray-700 text-sm font-bold mb-2">お問い合わせ内容</label>
<textarea {...register("text")} rows={8} className="resize-none shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-1 leading-tight focus:outline-none focus:shadow-outline"/>
<p className="text-red-500 text-xs italic mb-4">{errors.text?.message}</p>
</div>
<div className="flex items-center justify-center">
<button type="button" onClick={onOk} className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-8 rounded focus:outline-none focus:shadow-outline">
確認
</button>
</div>
<div className="flex items-center justify-center text-red-500 text-xs italic mt-2"></div>
</>
)
}
'use client';
import { useFormContext } from "react-hook-form";
import {SendMessageAction } from "./action";
type ContactFormType = {
name: string;
email: string;
message: string;
}
type Props = {
open: boolean;
onOk: () => void;
onCancel: () => void;
}
export default function Confirm({onOk, onCancel, open}:Props) {
const methods = useFormContext<ContactFormType>();
const {
getValues,
handleSubmit,
} = methods;
const SendAction = (data :ContactFormType) => {
SendMessageAction(data)
};
return open ?(
<>
<form onSubmit={handleSubmit(SendAction)} className="bg-white top-1/4 left-1/2 transform -translate-x-1/2 w-1/4 p-5 flex flex-col items-start absolute z-20">
<h2 className="text-xl font-bold mb-5">送信内容の確認</h2>
<label className="text-gray-700 text-sm font-bold mb-2">お名前</label>
<p className="text-lg mb-5">{ getValues('name') }</p>
<label className="text-gray-700 text-sm font-bold mb-2">メールアドレス</label>
<p className="text-lg mb-5 w-full overflow-hidden">{ getValues('email') }</p>
<label className="text-gray-700 text-sm font-bold mb-2">お問い合わせ内容</label>
<p className="text-lg mb-5 break-all max-h-60 overflow-y-auto">{ getValues('message') }</p>
<div className="flex mt-auto w-full">
<button type="submit" className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
送信
</button>
<button type="button" onClick={ onCancel } className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
キャンセル
</button>
</div>
</form>
{/*モーダルウインドウ以外をクリックした場合の処理*/}
<div onClick={ onCancel } className="fixed bg-black bg-opacity-50 w-full h-full top-0 left-0 z-10"/>
</>
):(<></>);
};
モーダル表示を作り変えた...
FIX
・ReactHookFormをのFormProviderを使ったFormをAction属性で飛ばすと、中身が空っぽのデータが送られるんだけど、これどうやって解決すればいいのかわからない。
-> <Form> のActionが使えないと、useFormStatus Pendingが使えない...
-> 現状は、button subimit を押下すると。const action が実行されて、送信中は追加送信できないように変更。
-> 【TODO:送信中の実装待ち】
・ちょうど、useFormState から useActionStateなるものに変更されたみたい
-> 色んな人の記事を漁るに、useFormStateにpennding機能を追加したものみたい。
-> https://react.dev/reference/react/useActionState リファレンスはまだ、書き換え途中?
-> Next.jsで試してみたけど、なにやらエラーが発生する...
-> TypeError: (0 , react__WEBPACK_IMPORTED_MODULE_2__.useActionState) is not a function or its return value is not iterable
-> Next.js canary版にしたら出来る模様
-> useActionStateを使うと、onSubmit属性でも、pendingのやり取りができる。便利
'use client';
import { useFormContext } from "react-hook-form";
import { ContactFormType } from './types';
import { SendMessageAction } from "./action";
import { useActionState } from "react";
type Props = {
open: boolean;
onOk: () => void;
onCancel: () => void;
}
export default function Confirm({onOk, onCancel, open}:Props) {
const methods = useFormContext<ContactFormType>();
const {
getValues,
handleSubmit,
} = methods;
const [formState, sendAction, pending] = useActionState(SendMessageAction, {"success": false, "message" : ""});
const action = (params :ContactFormType) => {
formState.success = true;
const formData = new FormData()
Object.entries(params).forEach(([key, value]) => {
formData.append(key, value)
})
sendAction(formData);
};
return open ?(
<>
<form onSubmit={handleSubmit(action)} className="bg-white top-1/4 left-1/2 transform -translate-x-1/2 w-1/4 p-5 flex flex-col items-start absolute z-20">
<h2 className="text-xl font-bold mb-5">送信内容の確認</h2>
<label className="text-gray-700 text-sm font-bold mb-2">お名前</label>
<p className="text-lg mb-5">{ getValues('name') }</p>
<label className="text-gray-700 text-sm font-bold mb-2">メールアドレス</label>
<p className="text-lg mb-5 w-full overflow-hidden">{ getValues('email') }</p>
<label className="text-gray-700 text-sm font-bold mb-2">お問い合わせ内容</label>
<p className="text-lg mb-5 break-all max-h-60 overflow-y-auto">{ getValues('message') }</p>
<div className="flex mt-auto w-full">
<button type="submit" disabled={pending || formState.success} className={`px-8 py-2 rounded text-white ${formState.success ? "bg-gray-500 cursor-not-allowed" : "bg-blue-600 hover:bg-blue-700"} px-8 py-2 mx-auto`}>
{pending ? "送信中..." : "送信"}
</button>
<button type="button" onClick={ onCancel } className="bg-slate-900 hover:bg-slate-700 text-white px-8 py-2 mx-auto">
キャンセル
</button>
</div>
<div className={`flex justify-center mt-4 ${formState.success ? '' : 'text-red-600'}`}>{formState.message}</div>
</form>
{/*モーダルウインドウ以外をクリックした場合の処理*/}
<div onClick={ onCancel } className="fixed bg-black bg-opacity-50 w-full h-full top-0 left-0 z-10"/>
</>
):(<></>);
};
action.ts はSendGrid Next.js App RouterとServer Actionsを使ってメールを送るフォームを作成するのuse server を使用。
$ app/form/ 下に新規作成
export type ContactFormType = {
name: string;
email: string;
message: string;
}