Formulaire de contact React qui envoie un e-mail sans backend

Formgong team ·

Envoyez FormData avec fetch vers le point Formgong et collez votre clé d’accès publique. Formgong filtre le spam et livre la demande par e-mail et par Telegram. Telegram est immédiat. Sur l’offre gratuite, l’e-mail est un résumé du jour. Vous ne tenez ni serveur, ni SMTP, ni clé secrète. Collez le composant ci-dessous.

Créez un formulaire, copiez la clé d’accès et remplacez fk_your_access_key dans le composant. La clé est publique. Sa place est dans ce fichier.

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

Autres montages React

Vite ou Create React App

Le composant ci-dessus est ce montage. Enregistrez-le dans src/ContactForm.jsx et affichez-le depuis App. Aucun paquet en plus.

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

Ajoutez la directive client en haut du fichier. La page qui affiche le formulaire peut rester un Server Component.

Next.js App Router
"use client";

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

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

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

Installez react-hook-form. Gardez sa validation. L’envoi poste quand même FormData vers Formgong.

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

// 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 { 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", "fr");
    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 || "Échec de l’envoi. Veuillez réessayer.");
        return;
      }
      reset();
      setNotice(result.message || "Envoyé. Merci !");
    } catch {
      setFailed(true);
      setNotice("Connexion impossible. Veuillez réessayer.");
    }
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label>{"Nom" <input {...register("name", { required: "Obligatoire" })} 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: "Obligatoire" })} aria-invalid={errors.email ? true : undefined} autoComplete="email" /></label>
      {errors.email && <span>{errors.email.message}</span>}
      <label>{"Message" <textarea {...register("message", { required: "Obligatoire" })} 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 ? "Envoi…" : "Envoyer"}</button>
      {failed ? <p role="alert">{notice}</p> : <p role="status">{notice}</p>}
    </form>
  );
}
TypeScript

Le même composant, avec un type pour l’état et pour la réponse JSON. Enregistrez-le dans ContactForm.tsx.

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

// Collez votre clé : remplacez fk_your_access_key par la clé de votre formulaire 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", "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()) as SubmitResult;
      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>
  );
}

Composant React prêt à l’emploi sur GitHub: github.com/formgong/react-contact-form. Licence MIT : cliquez sur « Use this template » ou copiez les fichiers.

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

Ajouter à un projet existant: npx formgong init (npm: formgong)

Comment fonctionnent les formulaires dans React

React dessine le formulaire. Il n’envoie pas le courrier. Vite et Create React App livrent des fichiers statiques, le navigateur a donc besoin d’une URL qui accepte le POST.

Cette URL est Formgong. Le composant envoie FormData avec fetch vers /submit. Il joint votre access_key public, _lang et un champ botcheck vide. Formgong filtre le spam et livre la demande.

Ce qui change ici

  • Telegram arrive tout de suite, même sur l’offre gratuite. L’e-mail est inclus, en résumé quotidien pour un destinataire.
  • Les envois sont stockés dans l’UE, sur Cloudflare D1 sous juridiction de l’UE.
  • Le filtre anti-spam n’exige ni CAPTCHA ni cookies. Le pot de miel est toujours actif. Turnstile est facultatif.
  • La clé d’accès du composant est publique. Vous ne mettez ni mot de passe SMTP ni clé d’API privée dans le bundle.
  • Le texte d’erreur et la page de remerciement peuvent suivre la langue du visiteur. Il y a 12 langues.

Pourquoi pas EmailJS ni votre propre route API ?

Pourquoi pas EmailJS ni votre propre route API ?
EmailJSVotre route APIFormgong
Ce que vous ajoutezUn paquet npm, un service id, un template id et une clé publique.Une fonction serverless et un client SMTP comme Nodemailer.Une clé d’accès publique dans le composant de cette page.
E-mailUn e-mail immédiat depuis votre modèle. L’offre gratuite, c’est 200 requêtes par mois et 2 modèles.Un e-mail immédiat si votre fournisseur accepte le message.Un résumé quotidien sur l’offre gratuite. Chaque demande par e-mail est sur Pro et Business.
TelegramCe n’est pas dans EmailJS. Le service envoie un e-mail.Seulement si vous écrivez cet appel vous-même.Livraison immédiate, déjà sur l’offre gratuite.
Clés dans le navigateurLa clé publique doit rester publique. Gardez la privée sur un serveur.Le mot de passe SMTP reste dans l’environnement de l’hébergeur. C’est le point fort de cette voie.La clé d’accès est publique. Il n’y a pas de secret SMTP.
SpamVous réglez les limites dans le compte EmailJS. Le tutoriel Webtips de 2024 n’ajoute rien.Vous construisez le filtre.Un pot de miel est toujours actif. Pas de cookies. Un CAPTCHA n’est pas exigé.
Quota gratuit200 requêtes par mois, puis le surplus est rejeté.Le palier gratuit de l’hébergeur, plus le plafond de votre messagerie.300 envois par mois.
Où sont les donnéesEmailJS garde l’historique sur ses serveurs. Le gratuit garde 7 jours. La page des prix ne dit pas « UE seulement ».Là où votre hébergeur et votre messagerie les gardent.Dans l’UE, sur Cloudflare D1.

Choisissez EmailJS quand le courrier doit partir de votre boîte, avec votre modèle, au moment de l’envoi.

Choisissez une fonction Vercel ou Netlify quand l’envoi doit mettre à jour votre base ou vérifier que le visiteur est connecté.

Choisissez Formgong pour un formulaire de contact. Telegram est immédiat sur l’offre gratuite. L’e-mail y est un résumé du jour. Vous ne stockez pas de secret SMTP.

Les chiffres EmailJS viennent de la page des prix du 6 octobre 2026. La colonne route API est une fonction Vercel ou Netlify plus SMTP, pas le tarif d’un seul service.

Mise en place dans React

  1. Créez un formulaire dans Formgong et copiez la clé d’accès. Elle ressemble à fk_your_access_key.
  2. Collez le composant ci-dessus dans l’app et remplacez cette clé.
  3. Affichez le composant, ouvrez le site et envoyez-vous un essai.
  4. Dans Formgong, ouvrez le formulaire et reliez Telegram si vous voulez une alerte sur le téléphone.

Questions et réponses

Faut-il un backend pour un formulaire de contact React ?

Non. Envoyez FormData avec fetch vers Formgong. Vous n’ajoutez ni route API, ni Nodemailer, ni SMTP.

Comment envoyer un e-mail depuis React sans Nodemailer ni SMTP ?

Utilisez le composant de cette page. Formgong accepte le POST et filtre le spam. Telegram reçoit la demande tout de suite. Sur l’offre gratuite, l’e-mail est un résumé du jour. Pro et Business vous écrivent à chaque envoi.

Est-il sûr de mettre la clé dans le code client ?

Oui. La access_key est publique. Elle ne peut qu’envoyer ce formulaire. Elle ne lit pas la boîte. En Pro, vous pouvez limiter les domaines autorisés. Ne mettez pas un mot de passe SMTP dans le bundle.

Comment arrêter le spam sur un formulaire React ?

Laissez botcheck vide. Formgong note le spam sans cookies et sans CAPTCHA obligatoire. Turnstile est facultatif si vous voulez plus tard une énigme.

Est-ce que cela marche avec Next.js ?

Oui. Dans l’App Router, ajoutez "use client" en haut du fichier. Le composant reste le même. La variante Next.js de cette page montre le fichier.

Commencer gratuitement