バックエンドなしでメールを送るReactの問い合わせフォーム

Formgong team ·

FormDataをfetchでFormgongのエンドポイントへ送り、公開のアクセスキーを貼ります。Formgongはスパムを除き、問い合わせをメールとTelegramへ届けます。Telegramはすぐ届きます。無料プランのメールは日次のダイジェストです。サーバーもSMTPも秘密鍵も要りません。下のコンポーネントを貼ってください。

フォームを作り、アクセスキーをコピーし、コンポーネントの fk_your_access_key を置き換えます。キーは公開です。置く場所はこのファイルです。

Vite または Create React App
import { useState } from "react";

// キーを貼る: fk_your_access_key を Formgong のフォームのキーに置き換えてください。
const ACCESS_KEY = "fk_your_access_key";

export default function ContactForm() {
  const [status, setStatus] = useState("idle");
  const [notice, setNotice] = useState("");

  async function onSubmit(event) {
    event.preventDefault();
    const form = event.currentTarget;
    if (!(form instanceof HTMLFormElement) || !form.reportValidity()) return;
    const body = new FormData(form);
    body.set("access_key", ACCESS_KEY);
    body.set("_lang", "ja");
    setStatus("sending");
    setNotice("");
    try {
      const response = await fetch("https://formgong.com/submit", {
        method: "POST",
        headers: { Accept: "application/json" },
        body,
      });
      const result = await response.json();
      if (!response.ok || !result.success) {
        setStatus("error");
        setNotice(result.message || "送信できませんでした。もう一度お試しください。");
        return;
      }
      form.reset();
      setStatus("success");
      setNotice(result.message || "送信しました。ありがとうございます。");
    } catch {
      setStatus("error");
      setNotice("接続できませんでした。もう一度お試しください。");
    }
  }

  const sending = status === "sending";
  return (
    <form onSubmit={onSubmit}>
      <label>{"お名前"} <input name="name" autoComplete="name" placeholder={"山田 太郎"} required /></label>
      <label>{"メールアドレス"} <input name="email" type="email" autoComplete="email" placeholder={"you@example.com"} required /></label>
      <label>{"メッセージ"} <textarea name="message" placeholder={"ご用件をご記入ください"} required /></label>
      <div aria-hidden="true" style={{ position: "absolute", insetInlineStart: 0, top: 0, width: 1, height: 1, overflow: "hidden", clipPath: "inset(50%)" }}>
        <input name="botcheck" tabIndex={-1} autoComplete="off" />
      </div>
      <button type="submit" disabled={sending}>{sending ? "送信中…" : "送信"}</button>
      <p role={status === "error" ? "alert" : "status"}>{notice}</p>
    </form>
  );
}

ほかのReactの形

Vite または Create React App

上のコンポーネントがこの形です。src/ContactForm.jsx として保存し、App から表示します。追加のパッケージはありません。

Vite または Create React App
// src/App.jsx — Vite or Create React App
import ContactForm from "./ContactForm";

export default function App() {
  return <ContactForm />;
}
Next.js App Router

ファイルの先頭にクライアント指示を足します。フォームを表示するページは Server Component のままで構いません。

Next.js App Router
"use client";

import { useState } from "react";

// キーを貼る: fk_your_access_key を Formgong のフォームのキーに置き換えてください。
const ACCESS_KEY = "fk_your_access_key";

export default function ContactForm() {
  const [status, setStatus] = useState("idle");
  const [notice, setNotice] = useState("");

  async function onSubmit(event) {
    event.preventDefault();
    const form = event.currentTarget;
    if (!(form instanceof HTMLFormElement) || !form.reportValidity()) return;
    const body = new FormData(form);
    body.set("access_key", ACCESS_KEY);
    body.set("_lang", "ja");
    setStatus("sending");
    setNotice("");
    try {
      const response = await fetch("https://formgong.com/submit", {
        method: "POST",
        headers: { Accept: "application/json" },
        body,
      });
      const result = await response.json();
      if (!response.ok || !result.success) {
        setStatus("error");
        setNotice(result.message || "送信できませんでした。もう一度お試しください。");
        return;
      }
      form.reset();
      setStatus("success");
      setNotice(result.message || "送信しました。ありがとうございます。");
    } catch {
      setStatus("error");
      setNotice("接続できませんでした。もう一度お試しください。");
    }
  }

  const sending = status === "sending";
  return (
    <form onSubmit={onSubmit}>
      <label>{"お名前"} <input name="name" autoComplete="name" placeholder={"山田 太郎"} required /></label>
      <label>{"メールアドレス"} <input name="email" type="email" autoComplete="email" placeholder={"you@example.com"} required /></label>
      <label>{"メッセージ"} <textarea name="message" placeholder={"ご用件をご記入ください"} required /></label>
      <div aria-hidden="true" style={{ position: "absolute", insetInlineStart: 0, top: 0, width: 1, height: 1, overflow: "hidden", clipPath: "inset(50%)" }}>
        <input name="botcheck" tabIndex={-1} autoComplete="off" />
      </div>
      <button type="submit" disabled={sending}>{sending ? "送信中…" : "送信"}</button>
      <p role={status === "error" ? "alert" : "status"}>{notice}</p>
    </form>
  );
}

// app/contact/page.jsx — this file can stay a Server Component.
import ContactForm from "./ContactForm";

export default function Page() {
  return <ContactForm />;
}
React Hook Form

react-hook-form を入れます。検証はそこに残します。送信は引き続き FormData を Formgong へ送ります。

React Hook Form
import { useState } from "react";
import { useForm } from "react-hook-form";

// キーを貼る: fk_your_access_key を Formgong のフォームのキーに置き換えてください。
const ACCESS_KEY = "fk_your_access_key";

export default function ContactForm() {
  const { register, handleSubmit, reset, formState: { errors, isSubmitting } } = useForm();
  const [notice, setNotice] = useState("");
  const [failed, setFailed] = useState(false);

  async function onSubmit(values) {
    const body = new FormData();
    body.set("access_key", ACCESS_KEY);
    body.set("_lang", "ja");
    body.set("name", values.name);
    body.set("email", values.email);
    body.set("message", values.message);
    body.set("botcheck", values.botcheck || "");
    setFailed(false);
    setNotice("");
    try {
      const response = await fetch("https://formgong.com/submit", {
        method: "POST",
        headers: { Accept: "application/json" },
        body,
      });
      const result = await response.json();
      if (!response.ok || !result.success) {
        setFailed(true);
        setNotice(result.message || "送信できませんでした。もう一度お試しください。");
        return;
      }
      reset();
      setNotice(result.message || "送信しました。ありがとうございます。");
    } catch {
      setFailed(true);
      setNotice("接続できませんでした。もう一度お試しください。");
    }
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label>{"お名前" <input {...register("name", { required: "必須" })} aria-invalid={errors.name ? true : undefined} autoComplete="name" /></label>
      {errors.name && <span>{errors.name.message}</span>}
      <label>{"メールアドレス" <input type="email" {...register("email", { required: "必須" })} aria-invalid={errors.email ? true : undefined} autoComplete="email" /></label>
      {errors.email && <span>{errors.email.message}</span>}
      <label>{"メッセージ" <textarea {...register("message", { required: "必須" })} aria-invalid={errors.message ? true : undefined} /></label>
      {errors.message && <span>{errors.message.message}</span>}
      <div aria-hidden="true" style={{ position: "absolute", insetInlineStart: 0, top: 0, width: 1, height: 1, overflow: "hidden", clipPath: "inset(50%)" }}>
        <input {...register("botcheck")} tabIndex={-1} autoComplete="off" />
      </div>
      <button type="submit" disabled={isSubmitting}>{isSubmitting ? "送信中…" : "送信"}</button>
      {failed ? <p role="alert">{notice}</p> : <p role="status">{notice}</p>}
    </form>
  );
}
TypeScript

同じコンポーネントに、状態と JSON の結果の型を足したものです。ContactForm.tsx として保存します。

TypeScript
import { useState, type FormEvent } from "react";

// キーを貼る: fk_your_access_key を Formgong のフォームのキーに置き換えてください。
const ACCESS_KEY = "fk_your_access_key";

type Status = "idle" | "sending" | "success" | "error";
type SubmitResult = { success?: boolean; message?: string };

export default function ContactForm() {
  const [status, setStatus] = useState<Status>("idle");
  const [notice, setNotice] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const form = event.currentTarget;
    if (!form.reportValidity()) return;
    const body = new FormData(form);
    body.set("access_key", ACCESS_KEY);
    body.set("_lang", "ja");
    setStatus("sending");
    setNotice("");
    try {
      const response = await fetch("https://formgong.com/submit", {
        method: "POST",
        headers: { Accept: "application/json" },
        body,
      });
      const result = (await response.json()) as SubmitResult;
      if (!response.ok || !result.success) {
        setStatus("error");
        setNotice(result.message || "送信できませんでした。もう一度お試しください。");
        return;
      }
      form.reset();
      setStatus("success");
      setNotice(result.message || "送信しました。ありがとうございます。");
    } catch {
      setStatus("error");
      setNotice("接続できませんでした。もう一度お試しください。");
    }
  }

  const sending = status === "sending";
  return (
    <form onSubmit={onSubmit}>
      <label>{"お名前"} <input name="name" autoComplete="name" placeholder={"山田 太郎"} required /></label>
      <label>{"メールアドレス"} <input name="email" type="email" autoComplete="email" placeholder={"you@example.com"} required /></label>
      <label>{"メッセージ"} <textarea name="message" placeholder={"ご用件をご記入ください"} required /></label>
      <div aria-hidden="true" style={{ position: "absolute", insetInlineStart: 0, top: 0, width: 1, height: 1, overflow: "hidden", clipPath: "inset(50%)" }}>
        <input name="botcheck" tabIndex={-1} autoComplete="off" />
      </div>
      <button type="submit" disabled={sending}>{sending ? "送信中…" : "送信"}</button>
      <p role={status === "error" ? "alert" : "status"}>{notice}</p>
    </form>
  );
}

すぐ使える React コンポーネント(GitHub): github.com/formgong/react-contact-form. MIT ライセンス:「Use this template」をクリックするか、ファイルをコピーしてください。

React パッケージ: npm install @formgong/react (npm: @formgong/react)

既存のプロジェクトに追加: npx formgong init (npm: formgong)

Reactでのフォームの仕組み

Reactはフォームを描きます。メールは送りません。ViteとCreate React Appは静的ファイルだけを出すので、ブラウザにはPOSTを受け取るURLが要ります。

そのURLがFormgongです。コンポーネントは FormData を fetch で /submit へ送ります。公開の access_key、_lang、空の botcheck が一緒に行きます。Formgongはスパムを除いて問い合わせを届けます。

ここが違う点

  • Telegramは無料プランでもすぐに届きます。メールも含みます。受け手1人への日次ダイジェストです。
  • 送信データはEUに置きます。EU管轄のCloudflare D1です。
  • スパム対策にCAPTCHAもCookieも要りません。ハニーポットは常に有効です。Turnstileは任意です。
  • コンポーネントのアクセスキーは公開です。SMTPのパスワードや非公開のAPIキーはバンドルに入れません。
  • エラー文とサンクスページは訪問者の言語に合わせられます。言語は12です。

なぜEmailJSでも自前のAPIルートでもないのか

なぜEmailJSでも自前のAPIルートでもないのか
EmailJS自前のAPIルートFormgong
足すものnpmパッケージ、service id、template id、公開キー。サーバーレス関数と、NodemailerのようなSMTPクライアント。このページのコンポーネントにある公開アクセスキー1つ。
メール自分のテンプレートから即時送信。無料は月200リクエストとテンプレート2つ。メール事業者が受け取れば即時送信。無料プランは日次ダイジェスト。1件ごとのメールはProとBusiness。
TelegramEmailJSの機能ではありません。送るのはメールです。その呼び出しを自分で書いたときだけ。無料プランでも即時配信。
ブラウザの鍵公開キーは公開のままにします。非公開キーはサーバーに置きます。SMTPのパスワードはホストの環境変数に残ります。これがこの方法の強みです。アクセスキーは公開です。SMTPの秘密はありません。
スパム制限はEmailJSの管理画面で設定します。2024年のWebtips解説は何も足しません。フィルタは自分で作ります。ハニーポットは常に有効。Cookieなし。CAPTCHAは必須ではありません。
無料枠月200リクエスト。超えた分は捨てられます。ホストの無料枠と、メール事業者の上限。月300件。
データの場所EmailJSは履歴を自社サーバーに保管します。無料は7日。料金ページはEUのみとは書いていません。ホストとメール事業者が保管する場所。EUのCloudflare D1。

メールを自分の受信箱から、自分のテンプレートで、送信した瞬間に出したいならEmailJSを選びます。

送信で自分のデータベースを更新する、または訪問者がログイン済みか確かめるなら、VercelかNetlifyの関数を選びます。

問い合わせフォームならFormgongを選びます。無料プランでもTelegramは即時です。そのプランのメールは日次ダイジェストです。SMTPの秘密は置きません。

EmailJSの数字は2026年10月6日の料金ページのものです。APIルートの列は、1社の料金表ではなく、VercelまたはNetlifyの関数にSMTPを足したものです。

Reactでの設定手順

  1. Formgongでフォームを作り、アクセスキーをコピーします。見た目は fk_your_access_key です。
  2. 上のコンポーネントをアプリに貼り、そのキーを置き換えます。
  3. コンポーネントを表示し、サイトを開いて自分宛にテストを送ります。
  4. Formgongでフォームを開き、電話に知らせが欲しければTelegramを接続します。

よくある質問

Reactの問い合わせフォームにバックエンドは要りますか?

不要です。FormData を fetch でFormgongへ送ります。APIルートもNodemailerもSMTPも足しません。

NodemailerやSMTPなしでReactからメールを送るには?

このページのコンポーネントを使います。FormgongがPOSTを受け、スパムを除きます。Telegramにはすぐ届きます。無料プランのメールは日次ダイジェストです。ProとBusinessは1件ごとにメールします。

キーをクライアントのコードに置いて安全ですか?

はい。access_key は公開です。このフォームを送ることしかできません。受信箱は読めません。Proでは許可ドメインを制限できます。SMTPのパスワードはバンドルに入れないでください。

Reactのフォームでスパムを止めるには?

botcheck は空のままにします。FormgongはCookieなし、必須CAPTCHAなしでスパムを採点します。あとからパズルが欲しければTurnstileは任意です。

Next.jsでも動きますか?

はい。App Routerではファイルの先頭に "use client" を足します。コンポーネントはそのままです。このページのNext.js版がファイルを示します。

無料ではじめる