Next.js App RouterとServer Actionsを使ってメールを送るフォームを作成する

Next.js App RouterとServer Actionsを使ってメールを送るフォームを作成する

Next.jsのServer Actionsは、フォーム送信などのクライアントサイドのイベントから、サーバーサイドの処理を直接呼び出す機能です。
フォームを実装する場合、通常は/api/callのようなエンドポイントをアプリケーション内に定義してサーバサイドの処理を記述しますが、Server Actionsでは、そのような定義をせずに簡単に処理を書けます。

Server Actionsは、2023年10月末にリリースされたNext.js 14でstableとなりました。今回はこの機能を用い、フォームを送信したらTwilio SendGridからメールが送られる処理の実装例を紹介します。

なお、Server ActionsはNext.jsのApp Routerで利用することが前提です。以前のPages Routerで同様のフォームを作成する方法は過去のブログ記事で紹介しているのでそちらをご参照ください。

作成するアプリケーションの構成は、Pages Router版の記事と同じく以下の図のとおりです。

Next.jsとVercelで作ったフォームからSendGridでメール送信する方法

今回も前回同様Vercelにデプロイします。あらかじめ、Vercelアカウントの新規登録Vercel CLIのインストールを済ませてください。

基本の実装手順

初期セットアップ

まず、Next.jsアプリケーションを新規作成します。

npx create-next-app@latest

新規作成の際に聞かれるオプションは全てデフォルトのままで構いません。今回実装に用いたNext.jsのバージョンは14.0.1です。言語にはTypeScriptを用い、スタイリングにはTailwind CSSを使います。

上の操作によりいくつかファイルが作成されますが、最初にapp/globals.cssを編集します。

@tailwind base;
@tailwind components;
@tailwind utilities;

上記のように、4行目以降を全て削除して余計なスタイルが適用されないようにします。

Webアプリケーションのtitledescriptionapp/layout.tsxで定義されているmetadataで変更可能です。

export const metadata: Metadata = {
  title: "SendGridを使ったフォーム送信",
  description: "Next.js Server ActionsとSendGridを使ったフォーム送信のサンプルです。",
}

以下のコマンドを実行するとローカル環境で挙動が確認できます。

npm run dev

今回は、この時点で一旦Vercelにデプロイしてみましょう。
vercelコマンドを実行します。認証が求められたらVercelのアカウントでログインしてください。

vercel

発行されたURLにアクセスして、ローカル環境で確認できたものと同じページが表示されたらデプロイは成功です。

UIの作成

アプリケーションの実装に移ります。まずはUIを作成します。app/page.tsxを以下のように編集してください。

export default function Home() {
  return (
    <div className="max-w-md mx-auto mt-4 bg-white p-6 rounded shadow-md">
      <h1 className="text-xl font-bold mb-4">お問い合わせフォーム</h1>
      <form>
        <div className="mb-4">
          <label htmlFor="email" className="block text-sm font-medium text-gray-600">メールアドレス</label>
          <input type="email" id="email" name="email" required
            className="mt-1 p-2 w-full border rounded-md"/>
        </div>
        <div className="mb-4">
          <label htmlFor="content" className="block text-sm font-medium text-gray-600">お問い合わせ内容</label>
          <textarea id="content" name="content" rows={4} required
            className="mt-1 p-2 w-full border rounded-md"></textarea>
        </div>
        <div className="flex justify-center">
          <button type="submit"
            className="px-8 py-2 rounded text-white bg-blue-600 hover:bg-blue-700">
            送信
          </button>
        </div>
      </form>
    </div>
  )
}

npm run devコマンドで、ローカル環境で以下のようなフォームが表示できればOKです。
今はまだformactionを指定していないので、送信ボタンを押しても何も起こりません。ここに、SendGridでメール送信する処理を加えていきます。

UIの作成

SendGridのAPIキーの設定

メール送信に必要なSendGridのAPIキーを作成して、その値をVercelの環境変数として登録します。登録はVercel CLIで可能です。

vercel env add SENDGRID_API_KEY

このコマンドを実行すると、SENDGRID_API_KEYとして登録する値を聞かれるので、作成したAPIキーをコピー&ペーストします。

登録した環境変数は、Vercel CLIでローカルにpullできます。

vercel env pull

ローカルに.env.localという名前のファイルが作成されて環境変数が格納されます。

メール送信処理の実装

フォームの「送信」ボタン押下後の処理をServer Actionsを用いて実装します。
先ほど編集したapp/page.tsxHome関数の中に、非同期関数formActionを定義しましょう。

export default function Home() {
  async function formAction(formData: FormData) {
    'use server';
    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": "service@example.com"
      },
      "content": [
        {
          "type": "text/plain",
          "value": "以下の内容でお問い合わせを受け付けました。\r\n------\r\n" + formData.get("content")
        }
      ]
    };
    const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
      method: 'POST',
      headers: headers,
      body: JSON.stringify(requestBody)
    });
  }
return ( // 以後略

先頭の行に‘use server’と書くことで、Server Actionsを使うことを宣言しています。
この関数では、fetchを用いてSendGridのMail Send APIを呼び出し、問い合わせ者に質問内容の確認メールを送る処理を定義しています。Mail Send APIの詳細は割愛しますが、パラメータ等についてはドキュメントをご覧ください。

また、戻り値の中のformタグのaction属性で、上で定義した関数を指定するよう変更します。

+      <form action={formAction}>
-      <form>

これだけでメールが送信されるはずです。バックエンドの処理(Mail Send API呼び出し)をアプリケーション内でAPIとして定義する必要はありません。

ローカル環境で自分のメールアドレスと適当な内容を入力して「送信」ボタンを押し、メールが届くことを確認しましょう。もう一度production環境にデプロイすれば、最初のデプロイで発行されたURLでこのフォームが動作するようになります。

vercel --prod

もう一歩先の実装

以上のステップで最低限のフォームを作成できました。しかし、「送信」ボタンを押しても画面上に何も変化が起こらないため、正常にメール送信されたのかがわかりません。そこで簡単な改善例を紹介します。

useFormStateフックを用いたフォーム送信結果の表示

ReactのuseFormStateは、フォーム送信の結果に基づいて状態を管理することができるフックです。これを利用して、フォーム送信が正しく完了したかどうかを画面上に表示させます。

useFormStateフックはクライアントコンポーネントでのみ動作するため、app/page.tsxファイルの先頭に‘use client’ディレクティブを追加する必要があります。また、クライアントコンポーネントの中にServer Actionsを書くことはできないので、メール送信を行う関数は別ファイルactions.tsの中で定義し、page.tsxでそれをimportすることにします。

再度app/page.tsxを編集します。formAction関数を削除し、以下のハイライトした行を追加します。

'use client';
import { useFormState } from "react-dom";
import { submitInquiry } from "./actions"

export default function Home() {
  const [formState, formAction] = useFormState(submitInquiry, {});

  return (
    // ...
      <form action={formAction}>
    // formの最後に以下を追加
        <div className={`flex justify-center mt-4 ${formState.success ? '' : 'text-red-600'}`}>{formState.message}</div>
      </form>
    // ...

useFormStateの引数には、フォーム送信時に実行される関数と、初期状態の2つが入ります。ここでは初期状態は空のオブジェクトです。Server Actionsでメール送信する関数submitInquiryを、app/actions.tsの中で定義します。

'use server';

type State = {
  success?: boolean;
  message?: string;
};

export async function submitInquiry(prevState: State, formData: FormData) {
  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": "from@example.com"
    },
    "content": [
      {
        "type": "text/plain",
        "value": "以下の内容でお問い合わせを受け付けました。\r\n------\r\n" + formData.get("content")
      }
    ]
  };
  try {
    const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
      method: 'POST',
      headers: headers,
      body: JSON.stringify(requestBody)
    });
    if (response.ok) {
      return { 
        success: true,
        message: "お問い合わせを受け付けました。"
      };
    } else {
      return { 
        success: false,
        message: "お問い合わせの送信に失敗しました。"
      };
    }
  } catch (e) {
    return { 
      success: false,
      message: "お問い合わせの送信に失敗しました。"
    };
  }
}

簡単にコードの内容を説明します。
まず、先頭の行で‘use server’ディレクティブを宣言しています。
次に、状態をState型として定義しています。メール送信リクエストが成功したかどうかをbooleanで表すsuccessと、画面上に表示する文字列を格納するmessageからなるオブジェクトです。
その後に、関数submitInquiryを記述しています。useFormStateの引数に指定する、フォーム送信時に実行される関数は、以前の状態を第一引数にとり、フォームで送信されるデータを第二引数にとります。
メール送信処理自体は初めの手順で実装した時と同じですが、Mail Send APIの実行結果に応じて適切なStateを返却しています。

これで、フォームの「送信」ボタン押下後に、「お問い合わせを受け付けました。」もしくは「お問い合わせの送信に失敗しました。」が表示されるようになります。

useFormStatusフックを用いた送信状態の表示

ReactのuseFormStatusフックを使えば、フォーム送信中の状態を画面に表示することができます。

app/page.tsxuseFormStatusをimportし、送信ボタンのコンポーネントをSubmit関数として切り出して定義します。
useFormStatusの戻り値として変数pendingを受け取り、その値(trueまたはfalse)に応じてボタンの色と表示される文字列を変えています。

import { useFormState, useFormStatus } from "react-dom";  // useFormStatusをimport

function Submit() {
  const { pending } = useFormStatus();
  return (
    <button type="submit"
      className={`px-8 py-2 rounded text-white ${pending ? "bg-gray-500 cursor-not-allowed" : "bg-blue-600 hover:bg-blue-700"}`}
      disabled={pending}
    >
      {pending ? "送信中..." : "送信"}
    </button>
)
}

最後に、app/page.tsxHomeの戻り値の中にあるbuttonタグを、上で定義したSubmitに置き換えます。

+ <Submit />
- <button type="submit"
-   className="px-8 py-2 rounded text-white bg-blue-600 hover:bg-blue-700">
-   送信
- </button>

これで、ボタン押下時にボタンの色が変わり、文字が「送信中…」に変わるようになりました!

useFormStatusフックを用いた送信状態の表示

おわりに

Next.jsのApp RouterとServer Actionsを使って、SendGridでメール送信するフォームの実装例をご紹介しました。Server Actionsの登場により、フロントエンドとバックエンドの連携がより簡単に(曖昧に?)なっていくと思われます。
Next.jsでメール送信機能を実装する際は、お手軽にAPI連携できるSendGridをぜひお使いください。

アーカイブ

メールを成功の原動力に

開発者にもマーケターにも信頼されているメールサービスを利用して、
時間の節約、スケーラビリティ、メール配信に関する専門知識を手に入れましょう。