Formulaire de contact React qui envoie un e-mail sans backend
Formgong team ·
Envoyez FormData avec fetch vers le point Formgong et collez votre clé d’accès publique. Formgong filtre le spam et livre la demande par e-mail et par Telegram. Telegram est immédiat. Sur l’offre gratuite, l’e-mail est un résumé du jour. Vous ne tenez ni serveur, ni SMTP, ni clé secrète. Collez le composant ci-dessous.
Créez un formulaire, copiez la clé d’accès et remplacez fk_your_access_key dans le composant. La clé est publique. Sa place est dans ce fichier.
import { useState } from "react";
// Paste your key: replace fk_your_access_key with the key from your Formgong form.
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", "en");
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 || "Could not send. Please try again.");
return;
}
form.reset();
setStatus("success");
setNotice(result.message || "Sent. Thank you!");
} catch {
setStatus("error");
setNotice("Connection failed. Please try again.");
}
}
const sending = status === "sending";
return (
<form onSubmit={onSubmit}>
<label>{"Name"} <input name="name" autoComplete="name" placeholder={"Your name"} required /></label>
<label>{"Email"} <input name="email" type="email" autoComplete="email" placeholder={"you@example.com"} required /></label>
<label>{"Message"} <textarea name="message" placeholder={"How can we help?"} 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 ? "Sending…" : "Send"}</button>
<p role={status === "error" ? "alert" : "status"}>{notice}</p>
</form>
);
}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", "uk");
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>{"Email"} <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>
);
}import { useState } from "react";
// Wklej klucz: zastąp fk_your_access_key kluczem formularza 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", "pl");
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 || "Nie udało się wysłać. Spróbuj ponownie.");
return;
}
form.reset();
setStatus("success");
setNotice(result.message || "Wysłano. Dziękujemy!");
} catch {
setStatus("error");
setNotice("Brak połączenia. Spróbuj ponownie.");
}
}
const sending = status === "sending";
return (
<form onSubmit={onSubmit}>
<label>{"Imię"} <input name="name" autoComplete="name" placeholder={"Twoje imię"} required /></label>
<label>{"E-mail"} <input name="email" type="email" autoComplete="email" placeholder={"ty@example.com"} required /></label>
<label>{"Wiadomość"} <textarea name="message" placeholder={"W czym możemy pomóc?"} 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 ? "Wysyłanie…" : "Wyślij"}</button>
<p role={status === "error" ? "alert" : "status"}>{notice}</p>
</form>
);
}import { useState } from "react";
// Anahtarınızı yapıştırın: fk_your_access_key değerini Formgong formunun anahtarıyla değiştirin.
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", "tr");
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 || "Gönderilemedi. Lütfen tekrar deneyin.");
return;
}
form.reset();
setStatus("success");
setNotice(result.message || "Gönderildi. Teşekkürler!");
} catch {
setStatus("error");
setNotice("Bağlantı kurulamadı. Lütfen tekrar deneyin.");
}
}
const sending = status === "sending";
return (
<form onSubmit={onSubmit}>
<label>{"Ad"} <input name="name" autoComplete="name" placeholder={"Adınız"} required /></label>
<label>{"E-posta"} <input name="email" type="email" autoComplete="email" placeholder={"siz@ornek.com"} required /></label>
<label>{"Mesaj"} <textarea name="message" placeholder={"Size nasıl yardımcı olabiliriz?"} 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 ? "Gönderiliyor…" : "Gönder"}</button>
<p role={status === "error" ? "alert" : "status"}>{notice}</p>
</form>
);
}import { useState } from "react";
// Pega tu clave: sustituye fk_your_access_key por la clave de tu formulario de 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", "es");
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 || "No se pudo enviar. Inténtalo de nuevo.");
return;
}
form.reset();
setStatus("success");
setNotice(result.message || "Enviado. ¡Gracias!");
} catch {
setStatus("error");
setNotice("Error de conexión. Inténtalo de nuevo.");
}
}
const sending = status === "sending";
return (
<form onSubmit={onSubmit}>
<label>{"Nombre"} <input name="name" autoComplete="name" placeholder={"Tu nombre"} required /></label>
<label>{"Correo electrónico"} <input name="email" type="email" autoComplete="email" placeholder={"tu@ejemplo.com"} required /></label>
<label>{"Mensaje"} <textarea name="message" placeholder={"¿En qué podemos ayudarte?"} 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 ? "Enviando…" : "Enviar"}</button>
<p role={status === "error" ? "alert" : "status"}>{notice}</p>
</form>
);
}import { useState } from "react";
// Collez votre clé : remplacez fk_your_access_key par la clé de votre formulaire 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", "fr");
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 || "Échec de l’envoi. Veuillez réessayer.");
return;
}
form.reset();
setStatus("success");
setNotice(result.message || "Envoyé. Merci !");
} catch {
setStatus("error");
setNotice("Connexion impossible. Veuillez réessayer.");
}
}
const sending = status === "sending";
return (
<form onSubmit={onSubmit}>
<label>{"Nom"} <input name="name" autoComplete="name" placeholder={"Votre nom"} required /></label>
<label>{"E-mail"} <input name="email" type="email" autoComplete="email" placeholder={"vous@exemple.com"} required /></label>
<label>{"Message"} <textarea name="message" placeholder={"Comment pouvons-nous vous aider ?"} 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 ? "Envoi…" : "Envoyer"}</button>
<p role={status === "error" ? "alert" : "status"}>{notice}</p>
</form>
);
}import { useState } from "react";
// Cole a sua chave: substitua fk_your_access_key pela chave do formulário 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", "pt");
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 || "Não foi possível enviar. Tente novamente.");
return;
}
form.reset();
setStatus("success");
setNotice(result.message || "Enviado. Obrigado!");
} catch {
setStatus("error");
setNotice("Falha na ligação. Tente novamente.");
}
}
const sending = status === "sending";
return (
<form onSubmit={onSubmit}>
<label>{"Nome"} <input name="name" autoComplete="name" placeholder={"O seu nome"} required /></label>
<label>{"E-mail"} <input name="email" type="email" autoComplete="email" placeholder={"voce@exemplo.com"} required /></label>
<label>{"Mensagem"} <textarea name="message" placeholder={"Como podemos ajudar?"} 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 ? "A enviar…" : "Enviar"}</button>
<p role={status === "error" ? "alert" : "status"}>{notice}</p>
</form>
);
}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", "ar");
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>
);
}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", "he");
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>
);
}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", "hi");
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>
);
}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>
);
}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>
);
}Autres montages React
Vite ou Create React App
Le composant ci-dessus est ce montage. Enregistrez-le dans src/ContactForm.jsx et affichez-le depuis App. Aucun paquet en plus.
// src/App.jsx — Vite or Create React App
import ContactForm from "./ContactForm";
export default function App() {
return <ContactForm />;
}Next.js App Router
Ajoutez la directive client en haut du fichier. La page qui affiche le formulaire peut rester un Server Component.
"use client";
import { useState } from "react";
// Collez votre clé : remplacez fk_your_access_key par la clé de votre formulaire 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", "fr");
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 || "Échec de l’envoi. Veuillez réessayer.");
return;
}
form.reset();
setStatus("success");
setNotice(result.message || "Envoyé. Merci !");
} catch {
setStatus("error");
setNotice("Connexion impossible. Veuillez réessayer.");
}
}
const sending = status === "sending";
return (
<form onSubmit={onSubmit}>
<label>{"Nom"} <input name="name" autoComplete="name" placeholder={"Votre nom"} required /></label>
<label>{"E-mail"} <input name="email" type="email" autoComplete="email" placeholder={"vous@exemple.com"} required /></label>
<label>{"Message"} <textarea name="message" placeholder={"Comment pouvons-nous vous aider ?"} 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 ? "Envoi…" : "Envoyer"}</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
Installez react-hook-form. Gardez sa validation. L’envoi poste quand même FormData vers Formgong.
import { useState } from "react";
import { useForm } from "react-hook-form";
// Collez votre clé : remplacez fk_your_access_key par la clé de votre formulaire 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", "fr");
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 || "Échec de l’envoi. Veuillez réessayer.");
return;
}
reset();
setNotice(result.message || "Envoyé. Merci !");
} catch {
setFailed(true);
setNotice("Connexion impossible. Veuillez réessayer.");
}
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>{"Nom" <input {...register("name", { required: "Obligatoire" })} 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: "Obligatoire" })} aria-invalid={errors.email ? true : undefined} autoComplete="email" /></label>
{errors.email && <span>{errors.email.message}</span>}
<label>{"Message" <textarea {...register("message", { required: "Obligatoire" })} 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 ? "Envoi…" : "Envoyer"}</button>
{failed ? <p role="alert">{notice}</p> : <p role="status">{notice}</p>}
</form>
);
}TypeScript
Le même composant, avec un type pour l’état et pour la réponse JSON. Enregistrez-le dans ContactForm.tsx.
import { useState, type FormEvent } from "react";
// Collez votre clé : remplacez fk_your_access_key par la clé de votre formulaire 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", "fr");
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 || "Échec de l’envoi. Veuillez réessayer.");
return;
}
form.reset();
setStatus("success");
setNotice(result.message || "Envoyé. Merci !");
} catch {
setStatus("error");
setNotice("Connexion impossible. Veuillez réessayer.");
}
}
const sending = status === "sending";
return (
<form onSubmit={onSubmit}>
<label>{"Nom"} <input name="name" autoComplete="name" placeholder={"Votre nom"} required /></label>
<label>{"E-mail"} <input name="email" type="email" autoComplete="email" placeholder={"vous@exemple.com"} required /></label>
<label>{"Message"} <textarea name="message" placeholder={"Comment pouvons-nous vous aider ?"} 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 ? "Envoi…" : "Envoyer"}</button>
<p role={status === "error" ? "alert" : "status"}>{notice}</p>
</form>
);
}Composant React prêt à l’emploi sur GitHub: github.com/formgong/react-contact-form. Licence MIT : cliquez sur « Use this template » ou copiez les fichiers.
Paquet React: npm install @formgong/react (npm: @formgong/react)
Ajouter à un projet existant: npx formgong init (npm: formgong)
Comment fonctionnent les formulaires dans React
React dessine le formulaire. Il n’envoie pas le courrier. Vite et Create React App livrent des fichiers statiques, le navigateur a donc besoin d’une URL qui accepte le POST.
Cette URL est Formgong. Le composant envoie FormData avec fetch vers /submit. Il joint votre access_key public, _lang et un champ botcheck vide. Formgong filtre le spam et livre la demande.
Ce qui change ici
- Telegram arrive tout de suite, même sur l’offre gratuite. L’e-mail est inclus, en résumé quotidien pour un destinataire.
- Les envois sont stockés dans l’UE, sur Cloudflare D1 sous juridiction de l’UE.
- Le filtre anti-spam n’exige ni CAPTCHA ni cookies. Le pot de miel est toujours actif. Turnstile est facultatif.
- La clé d’accès du composant est publique. Vous ne mettez ni mot de passe SMTP ni clé d’API privée dans le bundle.
- Le texte d’erreur et la page de remerciement peuvent suivre la langue du visiteur. Il y a 12 langues.
Pourquoi pas EmailJS ni votre propre route API ?
| EmailJS | Votre route API | Formgong | |
|---|---|---|---|
| Ce que vous ajoutez | Un paquet npm, un service id, un template id et une clé publique. | Une fonction serverless et un client SMTP comme Nodemailer. | Une clé d’accès publique dans le composant de cette page. |
| Un e-mail immédiat depuis votre modèle. L’offre gratuite, c’est 200 requêtes par mois et 2 modèles. | Un e-mail immédiat si votre fournisseur accepte le message. | Un résumé quotidien sur l’offre gratuite. Chaque demande par e-mail est sur Pro et Business. | |
| Telegram | Ce n’est pas dans EmailJS. Le service envoie un e-mail. | Seulement si vous écrivez cet appel vous-même. | Livraison immédiate, déjà sur l’offre gratuite. |
| Clés dans le navigateur | La clé publique doit rester publique. Gardez la privée sur un serveur. | Le mot de passe SMTP reste dans l’environnement de l’hébergeur. C’est le point fort de cette voie. | La clé d’accès est publique. Il n’y a pas de secret SMTP. |
| Spam | Vous réglez les limites dans le compte EmailJS. Le tutoriel Webtips de 2024 n’ajoute rien. | Vous construisez le filtre. | Un pot de miel est toujours actif. Pas de cookies. Un CAPTCHA n’est pas exigé. |
| Quota gratuit | 200 requêtes par mois, puis le surplus est rejeté. | Le palier gratuit de l’hébergeur, plus le plafond de votre messagerie. | 300 envois par mois. |
| Où sont les données | EmailJS garde l’historique sur ses serveurs. Le gratuit garde 7 jours. La page des prix ne dit pas « UE seulement ». | Là où votre hébergeur et votre messagerie les gardent. | Dans l’UE, sur Cloudflare D1. |
Choisissez EmailJS quand le courrier doit partir de votre boîte, avec votre modèle, au moment de l’envoi.
Choisissez une fonction Vercel ou Netlify quand l’envoi doit mettre à jour votre base ou vérifier que le visiteur est connecté.
Choisissez Formgong pour un formulaire de contact. Telegram est immédiat sur l’offre gratuite. L’e-mail y est un résumé du jour. Vous ne stockez pas de secret SMTP.
Les chiffres EmailJS viennent de la page des prix du 6 octobre 2026. La colonne route API est une fonction Vercel ou Netlify plus SMTP, pas le tarif d’un seul service.
Mise en place dans React
- Créez un formulaire dans Formgong et copiez la clé d’accès. Elle ressemble à
fk_your_access_key. - Collez le composant ci-dessus dans l’app et remplacez cette clé.
- Affichez le composant, ouvrez le site et envoyez-vous un essai.
- Dans Formgong, ouvrez le formulaire et reliez Telegram si vous voulez une alerte sur le téléphone.
Modèles et la documentation
Questions et réponses
Faut-il un backend pour un formulaire de contact React ?
Non. Envoyez FormData avec fetch vers Formgong. Vous n’ajoutez ni route API, ni Nodemailer, ni SMTP.
Comment envoyer un e-mail depuis React sans Nodemailer ni SMTP ?
Utilisez le composant de cette page. Formgong accepte le POST et filtre le spam. Telegram reçoit la demande tout de suite. Sur l’offre gratuite, l’e-mail est un résumé du jour. Pro et Business vous écrivent à chaque envoi.
Est-il sûr de mettre la clé dans le code client ?
Oui. La access_key est publique. Elle ne peut qu’envoyer ce formulaire. Elle ne lit pas la boîte. En Pro, vous pouvez limiter les domaines autorisés. Ne mettez pas un mot de passe SMTP dans le bundle.
Comment arrêter le spam sur un formulaire React ?
Laissez botcheck vide. Formgong note le spam sans cookies et sans CAPTCHA obligatoire. Turnstile est facultatif si vous voulez plus tard une énigme.
Est-ce que cela marche avec Next.js ?
Oui. Dans l’App Router, ajoutez "use client" en haut du fichier. Le composant reste le même. La variante Next.js de cette page montre le fichier.