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.

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

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.

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

Acrescente a diretiva de cliente no topo do ficheiro. A página que mostra o formulário pode continuar a ser um Server Component.

Next.js App Router
"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.

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

TypeScript
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?

Porque não EmailJS nem a sua própria rota de API?
EmailJSA sua rota de APIFormgong
O que adicionaUm 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.
E-mailCorreio 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.
TelegramNã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 browserA 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.
SpamOs 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átis200 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 dadosO 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

  1. Crie um formulário na Formgong e copie a chave de acesso. Parece-se com fk_your_access_key.
  2. Cole o componente acima na app e substitua essa chave.
  3. Mostre o componente, abra o site e envie um teste a si próprio.
  4. Na Formgong, abra o formulário e ligue o Telegram se quiser um aviso no telefone.

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.

Começar grátis