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-nodechanges, no plugin.useStateplusfetchcovers 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-form1import { 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
- Sign up at dashboard.formboost.app — free, no credit card
- Create a new endpoint and copy the URL
- Replace
YOUR_ENDPOINT_IDin the examples above