Formulário de contacto React que envia e-mail sem backend
Formgong team ·
Envie FormData com fetch para o endpoint da Formgong e cole a chave de acesso pública. A Formgong filtra o spam e entrega o pedido por e-mail e pelo Telegram. O Telegram é imediato. No plano grátis o e-mail é um resumo diário. Não corre um servidor, SMTP nem chaves secretas. Cole o componente abaixo.
Crie um formulário, copie a chave de acesso e substitua fk_your_access_key no componente. A chave é pública. O lugar dela é este ficheiro.
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>
);
}Outras variantes React
Vite ou Create React App
O componente acima é esta variante. Guarde-o como src/ContactForm.jsx e mostre-o a partir de App. Sem pacote extra.
// src/App.jsx — Vite or Create React App
import ContactForm from "./ContactForm";
export default function App() {
return <ContactForm />;
}Next.js App Router
Acrescente a diretiva de cliente no topo do ficheiro. A página que mostra o formulário pode continuar a ser um Server Component.
"use client";
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>
);
}
// app/contact/page.jsx — this file can stay a Server Component.
import ContactForm from "./ContactForm";
export default function Page() {
return <ContactForm />;
}React Hook Form
Instale react-hook-form. Deixe lá a validação. O envio continua a ser FormData para a Formgong.
import { useState } from "react";
import { useForm } from "react-hook-form";
// 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 { 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", "pt");
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 || "Não foi possível enviar. Tente novamente.");
return;
}
reset();
setNotice(result.message || "Enviado. Obrigado!");
} catch {
setFailed(true);
setNotice("Falha na ligação. Tente novamente.");
}
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>{"Nome" <input {...register("name", { required: "Obrigatório" })} 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: "Obrigatório" })} aria-invalid={errors.email ? true : undefined} autoComplete="email" /></label>
{errors.email && <span>{errors.email.message}</span>}
<label>{"Mensagem" <textarea {...register("message", { required: "Obrigatório" })} 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 ? "A enviar…" : "Enviar"}</button>
{failed ? <p role="alert">{notice}</p> : <p role="status">{notice}</p>}
</form>
);
}TypeScript
O mesmo componente, com um tipo para o estado e para a resposta JSON. Guarde-o como ContactForm.tsx.
import { useState, type FormEvent } from "react";
// Cole a sua chave: substitua fk_your_access_key pela chave do formulário 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", "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()) as SubmitResult;
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>
);
}Componente React pronto no GitHub: github.com/formgong/react-contact-form. Licença MIT: clique em “Use this template” ou copie os arquivos.
Pacote React: npm install @formgong/react (npm: @formgong/react)
Adicionar a um projeto existente: npx formgong init (npm: formgong)
Como funcionam os formulários em React
O React desenha o formulário. Não envia o correio. Vite e Create React App entregam ficheiros estáticos, por isso o browser precisa de um URL que aceite o POST.
Esse URL é a Formgong. O componente envia FormData com fetch para /submit. Seguem a access_key pública, _lang e um campo botcheck vazio. A Formgong filtra o spam e entrega o pedido.
O que é diferente aqui
- O Telegram chega logo, também no plano grátis. O e-mail entra também, como resumo diário para um destinatário.
- Os envios ficam na UE, no Cloudflare D1 sob jurisdição da UE.
- O filtro de spam não pede CAPTCHA nem cookies. O honeypot está sempre ligado. O Turnstile é opcional.
- A chave de acesso no componente é pública. Não mete uma palavra-passe SMTP nem uma chave privada de API no bundle.
- O texto de erro e a página de agradecimento podem seguir a língua do visitante. São 12 línguas.
Porque não EmailJS nem a sua própria rota de API?
| EmailJS | A sua rota de API | Formgong | |
|---|---|---|---|
| O que adiciona | Um pacote npm, um service id, um template id e uma chave pública. | Uma função serverless e um cliente SMTP como o Nodemailer. | Uma chave de acesso pública no componente desta página. |
| Correio imediato a partir do seu modelo. O plano grátis são 200 pedidos por mês e 2 modelos. | Correio imediato se o fornecedor aceitar a mensagem. | Um resumo diário no plano grátis. Cada pedido por e-mail está no Pro e no Business. | |
| Telegram | Não faz parte do EmailJS. O serviço envia e-mail. | Só se escrever essa chamada você mesmo. | Entrega imediata, já no plano grátis. |
| Chaves no browser | A chave pública deve ser pública. Guarde a privada num servidor. | A palavra-passe SMTP fica no ambiente do host. É esse o ponto forte desta via. | A chave de acesso é pública. Não há segredo SMTP. |
| Spam | Os limites define-os no painel do EmailJS. O tutorial Webtips de 2024 não acrescenta nada. | O filtro constrói-o você. | O honeypot está sempre ligado. Sem cookies. Um CAPTCHA não é obrigatório. |
| Quota grátis | 200 pedidos por mês; o que passar é descartado. | O patamar grátis do host, mais o limite do fornecedor de correio. | 300 envios por mês. |
| Onde ficam os dados | O EmailJS guarda o histórico nos seus servidores. O grátis guarda 7 dias. A página de preços não diz que é só a UE. | Onde o host e o fornecedor de correio os guardarem. | Na UE, no Cloudflare D1. |
Escolha EmailJS quando o correio deve sair da sua caixa, com o seu modelo, no momento do envio.
Escolha uma função Vercel ou Netlify quando o envio deve atualizar a sua base ou verificar que o visitante iniciou sessão.
Escolha a Formgong para um formulário de contacto. O Telegram é imediato no plano grátis. O e-mail nesse plano é um resumo diário. Não guarda um segredo SMTP.
Os números do EmailJS vêm da página de preços de 6 de outubro de 2026. A coluna da rota API é uma função Vercel ou Netlify mais SMTP, não a tabela de preços de um só serviço.
Configuração em React
- Crie um formulário na Formgong e copie a chave de acesso. Parece-se com
fk_your_access_key. - Cole o componente acima na app e substitua essa chave.
- Mostre o componente, abra o site e envie um teste a si próprio.
- Na Formgong, abra o formulário e ligue o Telegram se quiser um aviso no telefone.
Modelos e a documentação
Perguntas e respostas
Preciso de um backend para um formulário de contacto em React?
Não. Envie FormData com fetch para a Formgong. Não acrescenta uma rota de API, Nodemailer nem SMTP.
Como envio um e-mail a partir de React sem Nodemailer nem SMTP?
Use o componente desta página. A Formgong aceita o POST e filtra o spam. O Telegram recebe o pedido de imediato. No plano grátis o e-mail é um resumo diário. Pro e Business enviam um e-mail por cada envio.
É seguro pôr a chave no código do cliente?
Sim. A access_key é pública. Só pode enviar este formulário. Não lê a caixa de entrada. No Pro pode limitar os domínios permitidos. Não ponha uma palavra-passe SMTP no bundle.
Como paro o spam num formulário React?
Deixe botcheck vazio. A Formgong pontua o spam sem cookies e sem um CAPTCHA obrigatório. O Turnstile é opcional se mais tarde quiser um desafio.
Isto funciona com Next.js?
Sim. No App Router, acrescente "use client" no topo do ficheiro. O componente fica igual. A variante Next.js nesta página mostra o ficheiro.