Back to blog
    By

    React Contact Form in Gatsby (No Backend)

    Add a working contact form to your Gatsby site using React hooks and fetch. Submissions go to Formboost — no serverless functions, no backend, no spam.

    Gatsby builds static sites, which means there's no server to receive form submissions. Most tutorials suggest Netlify Forms or writing a serverless function — but you can skip both. This guide shows how to add a contact form to any Gatsby site that just works, using React hooks and Formboost.

    Key takeaways

    • Gatsby builds to static files, so there is no server to receive a POST — the form posts to an external endpoint instead.
    • No serverless function, no gatsby-node changes, no plugin.
    • useState plus fetch covers validation, loading and error states in one component.
    • Works the same on Netlify, Vercel, GitHub Pages or any static host.

    Why Not Netlify Forms?

    Netlify Forms only work when your site is deployed on Netlify. Formboost works everywhere — Netlify, Vercel, GitHub Pages, S3, your own server, anywhere. And it gives you a proper submission dashboard, AI spam filtering, and webhooks — things Netlify Forms don't include out of the box.

    The Contact Form Component

    Create src/components/ContactForm.js (or .tsx):

    1import { useState } from "react"
    2
    3export default function ContactForm() {
    4  const [status, setStatus] = useState("idle") // idle | sending | sent | error
    5
    6  async function handleSubmit(e) {
    7    e.preventDefault()
    8    setStatus("sending")
    9
    10    const data = Object.fromEntries(new FormData(e.target))
    11
    12    try {
    13      const res = await fetch("https://formboost.app/f/YOUR_ENDPOINT_ID", {
    14        method: "POST",
    15        headers: { "Content-Type": "application/json" },
    16        body: JSON.stringify(data),
    17      })
    18
    19      if (!res.ok) throw new Error()
    20      setStatus("sent")
    21    } catch {
    22      setStatus("error")
    23    }
    24  }
    25
    26  if (status === "sent") {
    27    return <p>Thanks! We'll get back to you soon.</p>
    28  }
    29
    30  return (
    31    <form onSubmit={handleSubmit}>
    32      <div>
    33        <label htmlFor="name">Name</label>
    34        <input id="name" type="text" name="name" required />
    35      </div>
    36
    37      <div>
    38        <label htmlFor="email">Email</label>
    39        <input id="email" type="email" name="email" required />
    40      </div>
    41
    42      <div>
    43        <label htmlFor="message">Message</label>
    44        <textarea id="message" name="message" rows={5} />
    45      </div>
    46
    47      {status === "error" && (
    48        <p style={{ color: "red" }}>Something went wrong. Please try again.</p>
    49      )}
    50
    51      <button type="submit" disabled={status === "sending"}>
    52        {status === "sending" ? "Sending…" : "Send Message"}
    53      </button>
    54    </form>
    55  )
    56}

    Replace YOUR_ENDPOINT_ID with your Formboost endpoint ID.

    Using It in a Page

    1// src/pages/contact.js
    2import React from "gatsby"
    3import ContactForm from "../components/ContactForm"
    4
    5export default function ContactPage() {
    6  return (
    7    <main>
    8      <h1>Contact Us</h1>
    9      <ContactForm />
    10    </main>
    11  )
    12}
    13
    14export const Head = () => <title>Contact | My Site</title>

    TypeScript Version

    1// src/components/ContactForm.tsx
    2import { useState } from "react"
    3
    4type Status = "idle" | "sending" | "sent" | "error"
    5
    6export default function ContactForm() {
    7  const [status, setStatus] = useState<Status>("idle")
    8
    9  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    10    e.preventDefault()
    11    setStatus("sending")
    12
    13    const data = Object.fromEntries(new FormData(e.currentTarget))
    14
    15    try {
    16      const res = await fetch("https://formboost.app/f/YOUR_ENDPOINT_ID", {
    17        method: "POST",
    18        headers: { "Content-Type": "application/json" },
    19        body: JSON.stringify(data),
    20      })
    21
    22      if (!res.ok) throw new Error()
    23      setStatus("sent")
    24    } catch {
    25      setStatus("error")
    26    }
    27  }
    28
    29  if (status === "sent") return <p>Thanks! We'll be in touch.</p>
    30
    31  return (
    32    <form onSubmit={handleSubmit}>
    33      <input name="name" type="text" required placeholder="Name" />
    34      <input name="email" type="email" required placeholder="Email" />
    35      <textarea name="message" rows={5} placeholder="Message" />
    36      {status === "error" && <p>Something went wrong.</p>}
    37      <button type="submit" disabled={status === "sending"}>
    38        {status === "sending" ? "Sending…" : "Send"}
    39      </button>
    40    </form>
    41  )
    42}

    Adding Validation with react-hook-form

    For more control over validation and error messages:

    1npm install react-hook-form
    1import { useForm } from "react-hook-form"
    2
    3type FormValues = { name: string; email: string; message: string }
    4
    5export default function ContactForm() {
    6  const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormValues>()
    7
    8  async function onSubmit(data: FormValues) {
    9    const res = await fetch("https://formboost.app/f/YOUR_ENDPOINT_ID", {
    10      method: "POST",
    11      headers: { "Content-Type": "application/json" },
    12      body: JSON.stringify(data),
    13    })
    14    if (!res.ok) throw new Error("Submission failed")
    15  }
    16
    17  return (
    18    <form onSubmit={handleSubmit(onSubmit)}>
    19      <input {...register("name", { required: "Name is required" })} />
    20      {errors.name && <span>{errors.name.message}</span>}
    21
    22      <input type="email" {...register("email", { required: "Email is required" })} />
    23      {errors.email && <span>{errors.email.message}</span>}
    24
    25      <textarea {...register("message")} rows={5} />
    26
    27      <button type="submit" disabled={isSubmitting}>
    28        {isSubmitting ? "Sending…" : "Send"}
    29      </button>
    30    </form>
    31  )
    32}

    Getting Your Endpoint

    1. Sign up at dashboard.formboost.app — free, no credit card
    2. Create a new endpoint and copy the URL
    3. Replace YOUR_ENDPOINT_ID in the examples above

    Start collecting Gatsby form submissions free →