React форма на пошту без бекенду

Formgong team ·

Надішліть FormData через fetch на ендпоінт Formgong і вставте публічний ключ доступу. Formgong фільтрує спам і доставляє заявку на email і в Telegram. У Telegram вона приходить одразу. На безкоштовному плані email надходить щоденним дайджестом. Свій сервер, SMTP і секретні ключі не потрібні. Компонент нижче можна вставити як є.

Створіть форму, скопіюйте ключ доступу і замініть fk_your_access_key у компоненті. Ключ публічний. Йому місце в цьому файлі.

Vite або Create React App
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>
  );
}

Інші варіанти на React

Vite або Create React App

Компонент вище — саме цей варіант. Збережіть його як src/ContactForm.jsx і виведіть з App. Жодного додаткового пакета.

Vite або Create React App
// src/App.jsx — Vite or Create React App
import ContactForm from "./ContactForm";

export default function App() {
  return <ContactForm />;
}
Next.js App Router

Додайте директиву клієнта на початку файлу. Сторінка, яка рендерить форму, може лишитися серверним компонентом.

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.

React Hook Form
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.

TypeScript
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-роут?
EmailJSВаш API-роутFormgong
Що додаєтеПакет npm, service id, template id і публічний ключ.Serverless-функцію і SMTP-клієнт, наприклад Nodemailer.Один публічний ключ доступу в компоненті на цій сторінці.
EmailЛист одразу, з вашого шаблону. На безкоштовному плані 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

  1. Створіть форму у Formgong і скопіюйте ключ доступу. Він має вигляд fk_your_access_key.
  2. Вставте компонент вище в застосунок і замініть цей ключ.
  3. Виведіть компонент, відкрийте сайт і надішліть собі тест.
  4. У 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 на цій сторінці показує файл.

Почати безкоштовно