Formulario de contacto en React que envía email sin backend
Formgong team ·
Envía FormData con fetch al endpoint de Formgong y pega tu clave de acceso pública. Formgong filtra el spam y entrega el aviso por email y por Telegram. Telegram es al momento. En el plan gratis el email es un resumen diario. No montas servidor, SMTP ni claves secretas. Pega el componente de abajo.
Crea un formulario, copia la clave de acceso y sustituye fk_your_access_key en el componente. La clave es pública. Su sitio es este archivo.
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>
);
}Otras variantes de React
Vite o Create React App
El componente de arriba es esta variante. Guárdalo como src/ContactForm.jsx y muéstralo desde App. Sin paquete extra.
// src/App.jsx — Vite or Create React App
import ContactForm from "./ContactForm";
export default function App() {
return <ContactForm />;
}Next.js App Router
Añade la directiva de cliente al inicio del archivo. La página que pinta el formulario puede seguir siendo un Server Component.
"use client";
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>
);
}
// app/contact/page.jsx — this file can stay a Server Component.
import ContactForm from "./ContactForm";
export default function Page() {
return <ContactForm />;
}React Hook Form
Instala react-hook-form. Deja ahí la validación. El envío sigue siendo FormData hacia Formgong.
import { useState } from "react";
import { useForm } from "react-hook-form";
// 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 { 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", "es");
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 || "No se pudo enviar. Inténtalo de nuevo.");
return;
}
reset();
setNotice(result.message || "Enviado. ¡Gracias!");
} catch {
setFailed(true);
setNotice("Error de conexión. Inténtalo de nuevo.");
}
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>{"Nombre" <input {...register("name", { required: "Obligatorio" })} aria-invalid={errors.name ? true : undefined} autoComplete="name" /></label>
{errors.name && <span>{errors.name.message}</span>}
<label>{"Correo electrónico" <input type="email" {...register("email", { required: "Obligatorio" })} aria-invalid={errors.email ? true : undefined} autoComplete="email" /></label>
{errors.email && <span>{errors.email.message}</span>}
<label>{"Mensaje" <textarea {...register("message", { required: "Obligatorio" })} 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 ? "Enviando…" : "Enviar"}</button>
{failed ? <p role="alert">{notice}</p> : <p role="status">{notice}</p>}
</form>
);
}TypeScript
El mismo componente, con un tipo para el estado y para la respuesta JSON. Guárdalo como ContactForm.tsx.
import { useState, type FormEvent } 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";
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", "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()) as SubmitResult;
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>
);
}Componente React listo en GitHub: github.com/formgong/react-contact-form. Licencia MIT: pulsa «Use this template» o copia los archivos.
Paquete de React: npm install @formgong/react (npm: @formgong/react)
Añadir a un proyecto existente: npx formgong init (npm: formgong)
Cómo funcionan los formularios en React
React dibuja el formulario. No envía el correo. Vite y Create React App entregan archivos estáticos, así que el navegador necesita una URL que acepte el POST.
Esa URL es Formgong. El componente envía FormData con fetch a /submit. Van tu access_key público, _lang y un campo botcheck vacío. Formgong filtra el spam y entrega el aviso.
Qué cambia aquí
- Telegram llega al momento también en el plan gratis. El email va incluido, como resumen diario para un destinatario.
- Los envíos se guardan en la UE, en Cloudflare D1 bajo jurisdicción de la UE.
- El filtro de spam no pide CAPTCHA ni cookies. El honeypot está siempre activo. Turnstile es opcional.
- La clave de acceso del componente es pública. No metes una contraseña SMTP ni una clave privada de API en el bundle.
- El texto de error y la página de gracias pueden seguir el idioma del visitante. Hay 12 idiomas.
¿Por qué no EmailJS ni tu propia ruta API?
| EmailJS | Tu ruta de API | Formgong | |
|---|---|---|---|
| Qué añades | Un paquete npm, un service id, un template id y una clave pública. | Una función serverless y un cliente SMTP como Nodemailer. | Una clave de acceso pública en el componente de esta página. |
| Correo al momento desde tu plantilla. El plan gratis son 200 peticiones al mes y 2 plantillas. | Correo al momento si tu proveedor acepta el mensaje. | Un resumen diario en el plan gratis. Cada aviso por email está en Pro y Business. | |
| Telegram | No forma parte de EmailJS. El servicio envía email. | Solo si escribes tú esa llamada. | Entrega al momento, ya en el plan gratis. |
| Claves en el navegador | La clave pública debe ser pública. Guarda la privada en un servidor. | La contraseña SMTP se queda en el entorno del host. Esa es la fuerza de esta vía. | La clave de acceso es pública. No hay secreto SMTP. |
| Spam | Los límites los pones en el panel de EmailJS. La guía de Webtips de 2024 no añade nada. | El filtro lo construyes tú. | El honeypot está siempre activo. Sin cookies. Un CAPTCHA no es obligatorio. |
| Cupo gratis | 200 peticiones al mes; lo que pase se descarta. | El cupo gratis de tu host, más el límite de tu proveedor de correo. | 300 envíos al mes. |
| Dónde están los datos | EmailJS guarda el historial en sus servidores. El plan gratis guarda 7 días. La página de precios no dice que sea solo la UE. | Donde los guarden tu host y tu proveedor de correo. | En la UE, en Cloudflare D1. |
Elige EmailJS cuando el correo deba salir de tu buzón, con tu plantilla, en el momento del envío.
Elige una función de Vercel o Netlify cuando el envío deba actualizar tu base o comprobar que el visitante ha iniciado sesión.
Elige Formgong para un formulario de contacto. Telegram es inmediato en el plan gratis. El email de ese plan es un resumen diario. No guardas una clave SMTP.
Las cifras de EmailJS salen de su página de precios del 6 de octubre de 2026. La columna de la ruta API es una función de Vercel o Netlify más SMTP, no la lista de precios de un solo servicio.
Configuración en React
- Crea un formulario en Formgong y copia la clave de acceso. Se parece a
fk_your_access_key. - Pega el componente de arriba en la app y sustituye esa clave.
- Muestra el componente, abre el sitio y envíate una prueba.
- En Formgong, abre el formulario y conecta Telegram si quieres un aviso en el teléfono.
Plantillas y la documentación
Preguntas y respuestas
¿Hace falta un backend para un formulario de contacto en React?
No. Envía FormData con fetch a Formgong. No añades una ruta API, Nodemailer ni SMTP.
¿Cómo envío un email desde React sin Nodemailer ni SMTP?
Usa el componente de esta página. Formgong acepta el POST y filtra el spam. Telegram recibe el aviso al momento. En el plan gratis el email es un resumen diario. Pro y Business te escriben por cada envío.
¿Es seguro poner la clave en el código del cliente?
Sí. La access_key es pública. Solo puede enviar este formulario. No puede leer la bandeja. En Pro puedes limitar los dominios permitidos. No pongas una contraseña SMTP en el bundle.
¿Cómo paro el spam en un formulario React?
Deja botcheck vacío. Formgong puntúa el spam sin cookies y sin un CAPTCHA obligatorio. Turnstile es opcional si más tarde quieres un reto.
¿Funciona con Next.js?
Sí. En el App Router, añade "use client" al inicio del archivo. El componente sigue igual. La variante de Next.js en esta página muestra el archivo.