React-Kontaktformular ohne eigenes Backend

Formgong team ·

Senden Sie FormData per fetch an den Formgong-Endpunkt und fügen Sie den öffentlichen Zugangsschlüssel ein. Formgong filtert Spam und liefert die Anfrage an E-Mail und Telegram. Telegram ist sofort da. Im Gratis-Tarif ist die E-Mail ein täglicher Digest. Sie betreiben keinen Server, kein SMTP und keine geheimen Schlüssel. Die Komponente unten fügen Sie direkt ein.

Legen Sie ein Formular an, kopieren Sie den Zugangsschlüssel und ersetzen Sie fk_your_access_key in der Komponente. Der Schlüssel ist öffentlich. Er gehört in diese Datei.

Vite oder Create React App
import { useState } from "react";

// Schlüssel einfügen: ersetzen Sie fk_your_access_key durch den Schlüssel Ihres Formgong-Formulars.
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", "de");
    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 || "Das Senden ist fehlgeschlagen. Bitte versuchen Sie es erneut.");
        return;
      }
      form.reset();
      setStatus("success");
      setNotice(result.message || "Gesendet. Vielen Dank!");
    } catch {
      setStatus("error");
      setNotice("Die Verbindung ist fehlgeschlagen. Bitte versuchen Sie es erneut.");
    }
  }

  const sending = status === "sending";
  return (
    <form onSubmit={onSubmit}>
      <label>{"Name"} <input name="name" autoComplete="name" placeholder={"Ihr Name"} required /></label>
      <label>{"E-Mail"} <input name="email" type="email" autoComplete="email" placeholder={"name@beispiel.de"} required /></label>
      <label>{"Nachricht"} <textarea name="message" placeholder={"Wie können wir Ihnen helfen?"} 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 ? "Senden…" : "Senden"}</button>
      <p role={status === "error" ? "alert" : "status"}>{notice}</p>
    </form>
  );
}

Weitere React-Varianten

Vite oder Create React App

Die Komponente oben ist diese Variante. Speichern Sie sie als src/ContactForm.jsx und rendern Sie sie aus App. Kein Zusatzpaket.

Vite oder 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

Setzen Sie die Client-Direktive an den Dateianfang. Die Seite, die das Formular rendert, kann eine Server Component bleiben.

Next.js App Router
"use client";

import { useState } from "react";

// Schlüssel einfügen: ersetzen Sie fk_your_access_key durch den Schlüssel Ihres Formgong-Formulars.
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", "de");
    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 || "Das Senden ist fehlgeschlagen. Bitte versuchen Sie es erneut.");
        return;
      }
      form.reset();
      setStatus("success");
      setNotice(result.message || "Gesendet. Vielen Dank!");
    } catch {
      setStatus("error");
      setNotice("Die Verbindung ist fehlgeschlagen. Bitte versuchen Sie es erneut.");
    }
  }

  const sending = status === "sending";
  return (
    <form onSubmit={onSubmit}>
      <label>{"Name"} <input name="name" autoComplete="name" placeholder={"Ihr Name"} required /></label>
      <label>{"E-Mail"} <input name="email" type="email" autoComplete="email" placeholder={"name@beispiel.de"} required /></label>
      <label>{"Nachricht"} <textarea name="message" placeholder={"Wie können wir Ihnen helfen?"} 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 ? "Senden…" : "Senden"}</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

Installieren Sie react-hook-form. Die Prüfung bleibt dort. Der Handler sendet trotzdem FormData an Formgong.

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

// Schlüssel einfügen: ersetzen Sie fk_your_access_key durch den Schlüssel Ihres Formgong-Formulars.
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", "de");
    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 || "Das Senden ist fehlgeschlagen. Bitte versuchen Sie es erneut.");
        return;
      }
      reset();
      setNotice(result.message || "Gesendet. Vielen Dank!");
    } catch {
      setFailed(true);
      setNotice("Die Verbindung ist fehlgeschlagen. Bitte versuchen Sie es erneut.");
    }
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label>{"Name" <input {...register("name", { required: "Pflichtfeld" })} aria-invalid={errors.name ? true : undefined} autoComplete="name" /></label>
      {errors.name && <span>{errors.name.message}</span>}
      <label>{"E-Mail" <input type="email" {...register("email", { required: "Pflichtfeld" })} aria-invalid={errors.email ? true : undefined} autoComplete="email" /></label>
      {errors.email && <span>{errors.email.message}</span>}
      <label>{"Nachricht" <textarea {...register("message", { required: "Pflichtfeld" })} 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 ? "Senden…" : "Senden"}</button>
      {failed ? <p role="alert">{notice}</p> : <p role="status">{notice}</p>}
    </form>
  );
}
TypeScript

Dieselbe Komponente, mit einem Typ für den Status und für die JSON-Antwort. Speichern Sie sie als ContactForm.tsx.

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

// Schlüssel einfügen: ersetzen Sie fk_your_access_key durch den Schlüssel Ihres Formgong-Formulars.
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", "de");
    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 || "Das Senden ist fehlgeschlagen. Bitte versuchen Sie es erneut.");
        return;
      }
      form.reset();
      setStatus("success");
      setNotice(result.message || "Gesendet. Vielen Dank!");
    } catch {
      setStatus("error");
      setNotice("Die Verbindung ist fehlgeschlagen. Bitte versuchen Sie es erneut.");
    }
  }

  const sending = status === "sending";
  return (
    <form onSubmit={onSubmit}>
      <label>{"Name"} <input name="name" autoComplete="name" placeholder={"Ihr Name"} required /></label>
      <label>{"E-Mail"} <input name="email" type="email" autoComplete="email" placeholder={"name@beispiel.de"} required /></label>
      <label>{"Nachricht"} <textarea name="message" placeholder={"Wie können wir Ihnen helfen?"} 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 ? "Senden…" : "Senden"}</button>
      <p role={status === "error" ? "alert" : "status"}>{notice}</p>
    </form>
  );
}

Fertige React-Komponente auf GitHub: github.com/formgong/react-contact-form. MIT-Lizenz: Klicken Sie auf „Use this template“ oder kopieren Sie die Dateien.

React-Paket: npm install @formgong/react (npm: @formgong/react)

Zu einem bestehenden Projekt hinzufügen: npx formgong init (npm: formgong)

So funktionieren Formulare in React

React zeichnet das Formular. Es sendet keine Mail. Vite und Create React App liefern statische Dateien, der Browser braucht also eine URL, die den POST annimmt.

Diese URL ist Formgong. Die Komponente sendet FormData per fetch an /submit. Mit dabei sind der öffentliche access_key, _lang und ein leeres Feld botcheck. Formgong filtert Spam und liefert die Anfrage.

Was hier anders ist

  • Telegram kommt auch im Gratis-Tarif sofort an. E-Mail ist dabei, als täglicher Digest für einen Empfänger.
  • Eingänge liegen in der EU, in Cloudflare D1 unter EU-Zuständigkeit.
  • Der Spamschutz braucht kein CAPTCHA und keine Cookies. Das Honeypot ist immer an. Turnstile ist optional.
  • Der Zugangsschlüssel in der Komponente ist öffentlich. Ein SMTP-Passwort oder einen privaten API-Schlüssel legen Sie nicht ins Bundle.
  • Fehlertext und die Danke-Seite können der Sprache des Besuchers folgen. Es sind 12 Sprachen.

Warum nicht EmailJS oder eine eigene API-Route?

Warum nicht EmailJS oder eine eigene API-Route?
EmailJSIhre API-RouteFormgong
Was Sie hinzufügenEin npm-Paket, eine Service-Id, eine Template-Id und einen öffentlichen Schlüssel.Eine Serverless-Funktion und einen SMTP-Client wie Nodemailer.Einen öffentlichen Zugangsschlüssel in der Komponente auf dieser Seite.
E-MailSofortpost aus Ihrer Vorlage. Der Gratis-Tarif hat 200 Anfragen im Monat und 2 Vorlagen.Sofortpost, wenn Ihr Anbieter die Nachricht annimmt.Ein täglicher Digest im Gratis-Tarif. Jede Anfrage per E-Mail gibt es in Pro und Business.
TelegramNicht Teil von EmailJS. Der Dienst sendet E-Mail.Nur wenn Sie diesen Aufruf selbst schreiben.Sofort, schon im Gratis-Tarif.
Schlüssel im BrowserDer öffentliche Schlüssel soll öffentlich sein. Den privaten behalten Sie auf einem Server.Das SMTP-Passwort bleibt in der Host-Umgebung. Das ist die Stärke dieses Wegs.Der Zugangsschlüssel ist öffentlich. Ein SMTP-Geheimnis gibt es nicht.
SpamLimits setzen Sie im EmailJS-Konto. Das Webtips-Tutorial von 2024 fügt nichts hinzu.Den Filter bauen Sie selbst.Ein Honeypot ist immer an. Keine Cookies. Ein CAPTCHA ist nicht nötig.
Gratis-Kontingent200 Anfragen im Monat, danach werden weitere verworfen.Das Gratis-Kontingent Ihres Hosts plus das Limit Ihres Mail-Anbieters.300 Eingänge im Monat.
Wo die Daten liegenEmailJS speichert den Verlauf auf eigenen Servern. Gratis behält 7 Tage. Die Preisseite sagt nicht „nur EU“.Dort, wo Ihr Host und Ihr Mail-Anbieter sie speichern.In der EU, auf Cloudflare D1.

Wählen Sie EmailJS, wenn die Mail aus Ihrem Postfach kommen soll, mit Ihrer Vorlage, im Moment des Absendens.

Wählen Sie eine Vercel- oder Netlify-Funktion, wenn die Einsendung Ihre Datenbank aktualisieren oder prüfen soll, dass der Besucher angemeldet ist.

Wählen Sie Formgong für ein Kontaktformular. Telegram ist im Gratis-Tarif sofort da. E-Mail ist dort ein täglicher Digest. Ein SMTP-Passwort speichern Sie nicht.

Die EmailJS-Zahlen stammen von der Preisseite am 6. Oktober 2026. Die Spalte API-Route ist eine Vercel- oder Netlify-Funktion plus SMTP, keine Preisliste eines Anbieters.

Einrichtung in React

  1. Legen Sie ein Formular in Formgong an und kopieren Sie den Zugangsschlüssel. Er sieht aus wie fk_your_access_key.
  2. Fügen Sie die Komponente oben in die App ein und ersetzen Sie diesen Schlüssel.
  3. Rendern Sie die Komponente, öffnen Sie die Seite und senden Sie sich einen Test.
  4. Öffnen Sie in Formgong das Formular und verbinden Sie Telegram, wenn Sie einen Hinweis aufs Telefon wollen.

Fragen und Antworten

Braucht ein React-Kontaktformular ein Backend?

Nein. Senden Sie FormData per fetch an Formgong. Sie fügen keine API-Route, kein Nodemailer und kein SMTP hinzu.

Wie sende ich E-Mail aus React ohne Nodemailer oder SMTP?

Nutzen Sie die Komponente auf dieser Seite. Formgong nimmt den POST an und filtert Spam. Telegram bekommt die Anfrage sofort. Im Gratis-Tarif ist E-Mail ein täglicher Digest. Pro und Business mailen Sie bei jeder Einsendung.

Darf der Schlüssel im Client-Code stehen?

Ja. Der access_key ist öffentlich. Er kann nur dieses Formular absenden. Den Posteingang kann er nicht lesen. In Pro können Sie erlaubte Domains begrenzen. Ein SMTP-Passwort legen Sie nicht ins Bundle.

Wie stoppe ich Spam im React-Formular?

Lassen Sie botcheck leer. Formgong bewertet Spam ohne Cookies und ohne Pflicht-CAPTCHA. Turnstile ist optional, wenn Sie später ein Rätsel wollen.

Funktioniert das mit Next.js?

Ja. Setzen Sie im App Router "use client" an den Anfang der Datei. Die Komponente bleibt gleich. Die Next.js-Variante auf dieser Seite zeigt die Datei.

Kostenlos starten