React форма на пошту без бекенду
Formgong team ·
Надішліть FormData через fetch на ендпоінт Formgong і вставте публічний ключ доступу. Formgong фільтрує спам і доставляє заявку на email і в Telegram. У Telegram вона приходить одразу. На безкоштовному плані email надходить щоденним дайджестом. Свій сервер, SMTP і секретні ключі не потрібні. Компонент нижче можна вставити як є.
Створіть форму, скопіюйте ключ доступу і замініть fk_your_access_key у компоненті. Ключ публічний. Йому місце в цьому файлі.
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>
);
}Інші варіанти на React
Vite або Create React App
Компонент вище — саме цей варіант. Збережіть його як src/ContactForm.jsx і виведіть з App. Жодного додаткового пакета.
// src/App.jsx — Vite or Create React App
import ContactForm from "./ContactForm";
export default function App() {
return <ContactForm />;
}Next.js App Router
Додайте директиву клієнта на початку файлу. Сторінка, яка рендерить форму, може лишитися серверним компонентом.
"use client";
import { useState } from "react";
// Вставте ключ: замініть fk_your_access_key на ключ вашої форми Formgong.
const ACCESS_KEY = "fk_your_access_key";
export default function ContactForm() {
const [status, setStatus] = useState("idle");
const [notice, setNotice] = useState("");
async function onSubmit(event) {
event.preventDefault();
const form = event.currentTarget;
if (!(form instanceof HTMLFormElement) || !form.reportValidity()) return;
const body = new FormData(form);
body.set("access_key", ACCESS_KEY);
body.set("_lang", "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>
);
}
// app/contact/page.jsx — this file can stay a Server Component.
import ContactForm from "./ContactForm";
export default function Page() {
return <ContactForm />;
}React Hook Form
Встановіть react-hook-form. Валідацію лишіть у ньому. Обробник усе одно надсилає FormData у Formgong.
import { useState } from "react";
import { useForm } from "react-hook-form";
// Вставте ключ: замініть fk_your_access_key на ключ вашої форми Formgong.
const ACCESS_KEY = "fk_your_access_key";
export default function ContactForm() {
const { register, handleSubmit, reset, formState: { errors, isSubmitting } } = useForm();
const [notice, setNotice] = useState("");
const [failed, setFailed] = useState(false);
async function onSubmit(values) {
const body = new FormData();
body.set("access_key", ACCESS_KEY);
body.set("_lang", "uk");
body.set("name", values.name);
body.set("email", values.email);
body.set("message", values.message);
body.set("botcheck", values.botcheck || "");
setFailed(false);
setNotice("");
try {
const response = await fetch("https://formgong.com/submit", {
method: "POST",
headers: { Accept: "application/json" },
body,
});
const result = await response.json();
if (!response.ok || !result.success) {
setFailed(true);
setNotice(result.message || "Не вдалося надіслати. Спробуйте ще раз.");
return;
}
reset();
setNotice(result.message || "Надіслано. Дякуємо!");
} catch {
setFailed(true);
setNotice("Немає з’єднання. Спробуйте ще раз.");
}
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>{"Ім’я" <input {...register("name", { required: "Обов’язкове поле" })} aria-invalid={errors.name ? true : undefined} autoComplete="name" /></label>
{errors.name && <span>{errors.name.message}</span>}
<label>{"Email" <input type="email" {...register("email", { required: "Обов’язкове поле" })} aria-invalid={errors.email ? true : undefined} autoComplete="email" /></label>
{errors.email && <span>{errors.email.message}</span>}
<label>{"Повідомлення" <textarea {...register("message", { required: "Обов’язкове поле" })} aria-invalid={errors.message ? true : undefined} /></label>
{errors.message && <span>{errors.message.message}</span>}
<div aria-hidden="true" style={{ position: "absolute", insetInlineStart: 0, top: 0, width: 1, height: 1, overflow: "hidden", clipPath: "inset(50%)" }}>
<input {...register("botcheck")} tabIndex={-1} autoComplete="off" />
</div>
<button type="submit" disabled={isSubmitting}>{isSubmitting ? "Надсилаємо…" : "Надіслати"}</button>
{failed ? <p role="alert">{notice}</p> : <p role="status">{notice}</p>}
</form>
);
}TypeScript
Той самий компонент, з типом для стану і для JSON-відповіді. Збережіть як ContactForm.tsx.
import { useState, type FormEvent } from "react";
// Вставте ключ: замініть fk_your_access_key на ключ вашої форми Formgong.
const ACCESS_KEY = "fk_your_access_key";
type Status = "idle" | "sending" | "success" | "error";
type SubmitResult = { success?: boolean; message?: string };
export default function ContactForm() {
const [status, setStatus] = useState<Status>("idle");
const [notice, setNotice] = useState("");
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const form = event.currentTarget;
if (!form.reportValidity()) return;
const body = new FormData(form);
body.set("access_key", ACCESS_KEY);
body.set("_lang", "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()) as SubmitResult;
if (!response.ok || !result.success) {
setStatus("error");
setNotice(result.message || "Не вдалося надіслати. Спробуйте ще раз.");
return;
}
form.reset();
setStatus("success");
setNotice(result.message || "Надіслано. Дякуємо!");
} catch {
setStatus("error");
setNotice("Немає з’єднання. Спробуйте ще раз.");
}
}
const sending = status === "sending";
return (
<form onSubmit={onSubmit}>
<label>{"Ім’я"} <input name="name" autoComplete="name" placeholder={"Ваше ім’я"} required /></label>
<label>{"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>
);
}Готовий React-компонент на GitHub: github.com/formgong/react-contact-form. Ліцензія MIT: натисніть «Use this template» або скопіюйте файли.
React-пакет: npm install @formgong/react (npm: @formgong/react)
Додати в наявний проєкт: npx formgong init (npm: formgong)
Як працюють форми в React
React малює форму. Листа він не надсилає. Vite і Create React App віддають статичні файли, тож браузеру потрібна адреса, яка прийме POST.
Ця адреса — Formgong. Компонент надсилає FormData через fetch на /submit. Разом ідуть публічний access_key, _lang і порожнє поле botcheck. Formgong фільтрує спам і доставляє заявку.
Чим це відрізняється
- Telegram надходить одразу навіть на безкоштовному плані. Email теж є: щоденний дайджест для одного отримувача.
- Заявки зберігаються в ЄС, у Cloudflare D1 під юрисдикцією ЄС.
- Захист від спаму не потребує CAPTCHA і не ставить файлів cookie. Honeypot увімкнено завжди. Turnstile — за бажанням.
- Ключ доступу в компоненті публічний. Пароль SMTP чи приватний ключ API у бандл не кладуть.
- Текст помилок і сторінка подяки можуть іти мовою відвідувача. Мов дванадцять.
Чому не EmailJS і не власний API-роут?
| EmailJS | Ваш API-роут | Formgong | |
|---|---|---|---|
| Що додаєте | Пакет npm, service id, template id і публічний ключ. | Serverless-функцію і SMTP-клієнт, наприклад Nodemailer. | Один публічний ключ доступу в компоненті на цій сторінці. |
| Лист одразу, з вашого шаблону. На безкоштовному плані 200 запитів на місяць і 2 шаблони. | Лист одразу, якщо провайдер приймає повідомлення. | Щоденний дайджест на безкоштовному плані. Кожна заявка листом — на Pro і Business. | |
| Telegram | Це не частина EmailJS. Сервіс надсилає email. | Лише якщо ви самі напишете цей виклик. | Доставка одразу, вже на безкоштовному плані. |
| Ключі в браузері | Публічний ключ і має бути публічним. Приватний тримайте на сервері. | Пароль SMTP лишається в середовищі хоста. У цьому сила цього шляху. | Ключ доступу публічний. Секрету SMTP немає. |
| Спам | Ліміти задаєте в кабінеті EmailJS. Туторіал Webtips 2024 року не додає нічого. | Фільтр пишете ви. | Honeypot увімкнено завжди. Без cookie. CAPTCHA не обов’язкова. |
| Безкоштовний ліміт | 200 запитів на місяць, далі зайві запити відкидаються. | Безкоштовний тариф хоста плюс ліміт поштового провайдера. | 300 заявок на місяць. |
| Де лежать дані | EmailJS зберігає історію на своїх серверах. На безкоштовному плані 7 днів. Сторінка цін не каже, що це лише ЄС. | Там, де їх тримають ваш хост і поштовий провайдер. | У ЄС, на Cloudflare D1. |
Обирайте EmailJS, коли лист має вийти з вашої скриньки, за вашим шаблоном, одразу після відправки.
Обирайте функцію Vercel або Netlify, коли заявка має оновити вашу базу або перевірити, що відвідувач увійшов.
Обирайте Formgong для контактної форми. Telegram на безкоштовному плані миттєвий. Email там — щоденний дайджест. Пароль SMTP зберігати не треба.
Цифри EmailJS взяті зі сторінки цін 6 жовтня 2026 року. Колонка API-роуту — це функція Vercel або Netlify плюс SMTP, а не прайс одного сервісу.
Налаштування в React
- Створіть форму у Formgong і скопіюйте ключ доступу. Він має вигляд
fk_your_access_key. - Вставте компонент вище в застосунок і замініть цей ключ.
- Виведіть компонент, відкрийте сайт і надішліть собі тест.
- У Formgong відкрийте форму і підключіть Telegram, якщо хочете сповіщення на телефон.
Стартери і документація
Питання та відповіді
Чи потрібен бекенд для контактної форми на React?
Ні. Надішліть FormData через fetch у Formgong. API-роут, Nodemailer і SMTP не додаєте.
Як надіслати лист із React без Nodemailer і SMTP?
Візьміть компонент на цій сторінці. Formgong приймає POST і фільтрує спам. У Telegram заявка приходить одразу. На безкоштовному плані email — це щоденний дайджест. Pro і Business надсилають лист на кожну заявку.
Чи безпечно тримати ключ у клієнтському коді?
Так. access_key публічний. Він лише надсилає заявки на цю форму. Інбокс ним не прочитати. На Pro можна обмежити дозволені домени. Пароль SMTP у бандл не кладіть.
Як зупинити спам у формі React?
Залиште botcheck порожнім. Formgong оцінює спам без cookie і без обов’язкової CAPTCHA. Turnstile можна додати пізніше, якщо захочете перевірку.
Чи працює це з Next.js?
Так. В App Router додайте "use client" на початку файлу. Компонент лишається тим самим. Варіант Next.js на цій сторінці показує файл.