Resend と Astro Actions を使用したフォームを作成する方法を実装しながらまとめてみました。
はじめに
こちらの記事はResend と Astro Actions を使用したフォームを作成する方法を実装しながらまとめてみました。 誤った内容を書いている可能性があるため、もしお気づきの際にはご指摘いただけると幸いです。
デモ
今回のデモサイトは以下になります。
Resendとは
Resendは開発者向けのメール送信 API サービスです。
SDKとAPIトークンを使ってメール送信の処理を書くことができます。
インストールは以下のコマンドで行います。
npm add resendAstro Actionsとは
Astro Actionsは型安全なバックエンド関数を定義し、呼び出すことができるAstroの機能です。
アクションは、src/actions/index.tsを作成することでAstroがサーバーアクションとして読み込んでくれます。
データのフェッチやバリデーション(Zod)を自動的に行うことができます。
基本的な使い方は、公式のドキュメントがわかりやすいです。
公式ドキュメント - Astro ActionsReact Hook Form とは
React Hook FormはReactでフォームを作成するためのライブラリです。
不要な再レンダリングを防ぎ、フォームの入力値やエラーの管理を行うことができます。
インストールは以下のコマンドで行います。
npm add react-hook-form実装
実装について、詳細にみてみます。
デモサイト全体の処理の流れ
- [ユーザー入力]
- [React Hook Form] クライアントバリデーション (mode: ‘onBlur’)
- [確認画面] getValues() で入力値を表示
- [FormData] React の state → FormData に変換
- [Astro Actions] actions.send(formData) → サーバーへ送信
- [Zod バリデーション] サーバーサイドで再バリデーション
- [Resend API] メール送信(管理者通知 + ユーザー自動返信)
- [完了画面 or エラー画面]
useFormの処理
const { register, handleSubmit, reset, getValues, formState: { errors },} = useForm<FormValues>({ mode: "onBlur", defaultValues: { name: "", email: "", address: "", tel: "", select: "", message: "", privacy: false, },});今回処理に使用しているuseFormの引数と返り値について、以下のようになっています。
useFormの引数
| 引数 | 説明 |
|---|---|
mode | バリデーションのモードを指定する。onBlur、onChange、onSubmit、onTouched、allが指定でき、それぞれのイベントが発生した時にバリデーションを行う。 |
defaultValues | フォームの初期値を指定する。デフォルトでは空のオブジェクトが設定されている。 |
useFormの返り値
| 変数/関数 | 役割 |
|---|---|
register | input要素をフォームに紐付ける |
handleSubmit | バリデーションが成功した時に送信処理を実行する |
reset | フォーム値を初期化(または指定値にリセット)する |
getValues | 現在のフォーム値を取得する |
formState.errors | バリデーションエラーを参照する |
Astro Actionsの処理
Astro Actionsで記載する、SSRで行うAPI処理は以下のようになります。
行なっていることとしては、サーバーのアクションをsendとして定義し、データー形式をformとして受け取り、zodを使用してバリデーションを行っています。
バリデーションが成功すれば、handlerでResendのAPIを使用してメール送信の処理を行います。
import { ActionError, defineAction } from "astro:actions";import { z } from "astro:schema";import { RESEND_API_TOKEN } from "astro:env/server";import { Resend } from "resend";
export const server = { send: defineAction({ accept: "form", input: z.object({ name: z.string(), email: z.string().email(), postCode: z.string().optional(), address: z.string().optional(), tel: z .string() .regex(/^[\d-]+$/) .optional(),
select: z.string().optional(), message: z.string(), privacy: z.string().transform((val) => val === "on"), }), handler: async (input) => { const resend = new Resend(RESEND_API_TOKEN); const html = `<table> <tr> <td>名前</td> <td>${input.name}</td> </tr> <tr> <td>メールアドレス</td> <td>${input.email}</td> </tr> <tr> <td>住所</td> <td>${input.address}</td> </tr> <tr> <td>電話番号</td> <td>${input.tel}</td> </tr> <tr> <td>選択</td> <td>${input.select}</td> </tr> <tr> <td>お問い合わせ内容</td> <td>${input.message}</td> </tr> <tr> <td>プライバシーに同意して送信してください。</td> <td>${input.privacy ? "同意" : "不同意"}</td> </tr> </table>`;
const { data, error } = await resend.batch.send([ { from: "Acme <onboarding@resend.dev>", to: ["delivered@resend.dev"], subject: `${input.name} 様からお問い合わせがありました。`, html, }, { from: "Acme <onboarding@resend.dev>", to: ["delivered@resend.dev"], subject: "お問い合わせありがとうございました。", html: ` <p>${input.name} 様</p> <p>お問い合わせいただきありがとうございます。</p> <p>以下の内容で承りました。担当者より折り返しご連絡いたします。</p> ${html} `, }, ]); if (error) { throw new ActionError({ code: "BAD_REQUEST", message: error.message, }); } return data; }, }),};SSRでの処理は以下のように記述します。
今回はサーバーへの送信処理を書きたかったので、sendという名前で定義しています。
また、フォームの送信テストにResendで提供されているテスト用アドレスを使用しています。
export const server = { myActions: defineAction({}),};defineActionの引数ついて、以下のようになっています。
defineActionの引数
| 引数 | 説明 |
|---|---|
accept | 受け取るデータの形式を指定する。form、json、text、binaryが指定でき、それぞれの形式に応じてデータを受け取る。 |
input | データの型を指定する。z.object、z.array、z.string、z.number、z.boolean、z.date、z.enum、z.customが指定でき、それぞれの型に応じてデータを受け取る。 |
handler | データの処理を行う関数を指定する。 |
注意
環境変数の読み込みを、Astro
Actions内で記述するときは、通常の環境変数の読み込み方法とは異なり、astro:env/serverを使用して読み込む必要があります。
入力画面のステートの管理
- useStateで入力、確認、完了、エラーの状態管理
- onSubmit関数の進捗に応じて、状態を変更
type FormStepStatus = "input" | "confirm" | "success" | "error";
const [status, setStatus] = useState<FormStepStatus>("input");const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); if (!formData) return;
try { const fd = new FormData(); Object.entries(formData).forEach(([key, value]) => { if (key === "privacy" && value) { fd.append(key, "on"); } else if (value) { fd.append(key, String(value)); } });
const { error } = await actions.send(fd);
if (error) { throw new Error(error.message); }
setStatus("success"); reset(); } catch (error) { setStatus("error");
console.error(error); }};AstroActionsを使うとenvの読み方が変わる
サーバー処理で環境変数を使用する場合は、通常の呼び出し方とは異なるため注意が必要です。
より安全に環境変数を呼び出せるように、アストロ側でサーバー側で使用可能なものを制限できたり、型安全に扱えるようにスキーマを定義できます。
詳しくはAstroの公式ドキュメントがわかりやすいです。