Formularz kontaktowy React bez własnego backendu

Formgong team ·

Wyślij FormData przez fetch na endpoint Formgong i wklej publiczny klucz dostępu. Formgong filtruje spam i dostarcza zgłoszenie na e-mail oraz do Telegrama. Na Telegramie jest od razu. Na darmowym planie e-mail to dzienny skrót. Nie stawiasz serwera, SMTP ani sekretów. Komponent poniżej wklejasz od razu.

Utwórz formularz, skopiuj klucz dostępu i podmień fk_your_access_key w komponencie. Klucz jest publiczny. Jego miejsce jest w tym pliku.

Vite albo Create React App
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>
  );
}

Inne warianty React

Vite albo Create React App

Komponent wyżej jest właśnie tym wariantem. Zapisz go jako src/ContactForm.jsx i wyrenderuj z App. Bez dodatkowego pakietu.

Vite albo 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

Dodaj dyrektywę klienta na początku pliku. Strona, która renderuje formularz, może zostać komponentem serwerowym.

Next.js App Router
"use client";

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>
  );
}

// app/contact/page.jsx — this file can stay a Server Component.
import ContactForm from "./ContactForm";

export default function Page() {
  return <ContactForm />;
}
React Hook Form

Zainstaluj react-hook-form. Walidację zostaw w nim. Handler i tak wysyła FormData do Formgong.

React Hook Form
import { useState } from "react";
import { useForm } from "react-hook-form";

// Wklej klucz: zastąp fk_your_access_key kluczem formularza 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", "pl");
    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 || "Nie udało się wysłać. Spróbuj ponownie.");
        return;
      }
      reset();
      setNotice(result.message || "Wysłano. Dziękujemy!");
    } catch {
      setFailed(true);
      setNotice("Brak połączenia. Spróbuj ponownie.");
    }
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label>{"Imię" <input {...register("name", { required: "Pole wymagane" })} 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: "Pole wymagane" })} aria-invalid={errors.email ? true : undefined} autoComplete="email" /></label>
      {errors.email && <span>{errors.email.message}</span>}
      <label>{"Wiadomość" <textarea {...register("message", { required: "Pole wymagane" })} 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 ? "Wysyłanie…" : "Wyślij"}</button>
      {failed ? <p role="alert">{notice}</p> : <p role="status">{notice}</p>}
    </form>
  );
}
TypeScript

Ten sam komponent, z typem statusu i odpowiedzi JSON. Zapisz go jako ContactForm.tsx.

TypeScript
import { useState, type FormEvent } from "react";

// Wklej klucz: zastąp fk_your_access_key kluczem formularza 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", "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()) as SubmitResult;
      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>
  );
}

Gotowy komponent React na GitHubie: github.com/formgong/react-contact-form. Licencja MIT: kliknij „Use this template” albo skopiuj pliki.

Pakiet React: npm install @formgong/react (npm: @formgong/react)

Dodaj do istniejącego projektu: npx formgong init (npm: formgong)

Jak działają formularze w React

React rysuje formularz. Maila nie wysyła. Vite i Create React App oddają pliki statyczne, więc przeglądarka potrzebuje adresu, który przyjmie POST.

Tym adresem jest Formgong. Komponent wysyła FormData przez fetch na /submit. Idą z nim publiczny access_key, _lang i puste pole botcheck. Formgong filtruje spam i dostarcza zgłoszenie.

Czym to się różni

  • Telegram przychodzi od razu także na darmowym planie. E-mail też jest: dzienny skrót dla jednego odbiorcy.
  • Zgłoszenia są przechowywane w UE, w Cloudflare D1 pod jurysdykcją UE.
  • Ochrona przed spamem nie wymaga CAPTCHA ani ciasteczek. Honeypot jest zawsze włączony. Turnstile jest opcjonalny.
  • Klucz dostępu w komponencie jest publiczny. Hasła SMTP ani prywatnego klucza API nie wkładasz do bundla.
  • Tekst błędów i strona podziękowania mogą iść w języku odwiedzającego. Języków jest dwanaście.

Dlaczego nie EmailJS ani własna trasa API?

Dlaczego nie EmailJS ani własna trasa API?
EmailJSTwój route APIFormgong
Co dodajeszPakiet npm, service id, template id i klucz publiczny.Funkcję serverless i klienta SMTP, na przykład Nodemailer.Jeden publiczny klucz dostępu w komponencie na tej stronie.
E-mailMail od razu, z Twojego szablonu. Darmowy plan to 200 żądań miesięcznie i 2 szablony.Mail od razu, gdy dostawca przyjmie wiadomość.Dzienny skrót na darmowym planie. Każde zgłoszenie mailem jest na Pro i Business.
TelegramTo nie jest część EmailJS. Serwis wysyła e-mail.Tylko jeśli sam napiszesz to wywołanie.Dostawa od razu, już na darmowym planie.
Klucze w przeglądarceKlucz publiczny ma być publiczny. Prywatny trzymaj na serwerze.Hasło SMTP zostaje w środowisku hosta. To jest mocna strona tej drogi.Klucz dostępu jest publiczny. Sekretu SMTP nie ma.
SpamLimity ustawiasz w panelu EmailJS. Poradnik Webtips z 2024 roku nie dodaje nic.Filtr piszesz sam.Honeypot jest zawsze włączony. Bez ciasteczek. CAPTCHA nie jest wymagana.
Darmowy limit200 żądań miesięcznie, potem nadmiar jest odrzucany.Darmowy próg hosta plus limit dostawcy poczty.300 zgłoszeń miesięcznie.
Gdzie leżą daneEmailJS trzyma historię na swoich serwerach. Plan darmowy trzyma 7 dni. Strona cen nie mówi, że to tylko UE.Tam, gdzie trzyma je host i dostawca poczty.W UE, na Cloudflare D1.

Wybierz EmailJS, gdy mail ma wyjść z Twojej skrzynki, z Twojego szablonu, w chwili wysłania.

Wybierz funkcję Vercel albo Netlify, gdy zgłoszenie ma zaktualizować Twoją bazę albo sprawdzić, że odwiedzający jest zalogowany.

Wybierz Formgong do formularza kontaktowego. Telegram na darmowym planie jest natychmiastowy. E-mail na tym planie to dzienny skrót. Hasła SMTP nie przechowujesz.

Liczby EmailJS pochodzą ze strony cen z 6 października 2026. Kolumna route API to funkcja Vercel albo Netlify plus SMTP, a nie cennik jednej firmy.

Konfiguracja w React

  1. Utwórz formularz w Formgong i skopiuj klucz dostępu. Wygląda jak fk_your_access_key.
  2. Wklej komponent wyżej do aplikacji i podmień ten klucz.
  3. Wyrenderuj komponent, otwórz stronę i wyślij sobie test.
  4. W Formgong otwórz formularz i podepnij Telegram, jeśli chcesz alert na telefon.

Pytania i odpowiedzi

Czy formularz kontaktowy React potrzebuje backendu?

Nie. Wyślij FormData przez fetch do Formgong. Nie dodajesz trasy API, Nodemailera ani SMTP.

Jak wysłać e-mail z React bez Nodemailera i SMTP?

Użyj komponentu na tej stronie. Formgong przyjmuje POST i filtruje spam. Na Telegram zgłoszenie idzie od razu. Na darmowym planie e-mail to dzienny skrót. Pro i Business wysyłają mail przy każdym zgłoszeniu.

Czy klucz w kodzie przeglądarki jest bezpieczny?

Tak. access_key jest publiczny. Pozwala tylko wysłać zgłoszenie do tego formularza. Skrzynki nim nie odczytasz. W Pro możesz ograniczyć dozwolone domeny. Hasła SMTP nie wkładaj do bundla.

Jak zatrzymać spam w formularzu React?

Zostaw botcheck puste. Formgong ocenia spam bez ciasteczek i bez wymaganego CAPTCHA. Turnstile możesz dodać później, jeśli chcesz zagadkę.

Czy to działa z Next.js?

Tak. W App Router dodaj "use client" na początku pliku. Komponent zostaje ten sam. Wariant Next.js na tej stronie pokazuje plik.

Zacznij za darmo