Send email from a React form without a backend
React can't send email, so your form needs somewhere to post. Point it at a Nisuform endpoint and each submission is checked for spam, saved and emailed to you. Free for 100 submissions a month.
Free for 100 submissions a month, no credit card. Updated
React runs in the browser, and a browser can't send email. Most tutorials fix that with an API route that calls Resend or SendGrid. That means hosting a server, verifying a sending domain, keeping an API key secret and writing your own spam checks.
A form backend takes the place of that route. The form posts to a Nisuform endpoint, which checks it for spam, saves it to your inbox and emails it to you. Your app stays static and can live on any host.
A hook for any form
Copy this hook into your project. It reads the fields with FormData, adds the fill time the spam check uses, and turns the JSON response into a status.
import { useRef, useState, type FormEvent } from 'react'
type Status = 'idle' | 'sending' | 'sent' | 'error'
export function useFormBackend(endpoint: string) {
const shownAt = useRef(Date.now())
const [status, setStatus] = useState<Status>('idle')
const [error, setError] = useState('')
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
const data = new FormData(event.currentTarget)
data.set('_elapsed', String(Date.now() - shownAt.current))
setStatus('sending')
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { Accept: 'application/json' },
body: data,
})
const result = await response.json()
if (result.ok) {
setStatus('sent')
} else {
setError(result.message)
setStatus('error')
}
} catch {
setError('Something went wrong. Please try again.')
setStatus('error')
}
}
return { status, error, onSubmit }
}A contact form then needs only its markup:
import { useFormBackend } from './useFormBackend'
export function ContactForm() {
const { status, error, onSubmit } = useFormBackend('https://api.nisuform.com/s/YOUR_FORM_KEY')
if (status === 'sent') {
return <p role="status">Thanks! We got your message.</p>
}
return (
<form onSubmit={onSubmit}>
<label>
Name
<input name="name" type="text" autoComplete="name" required />
</label>
<label>
Email
<input name="email" type="email" autoComplete="email" required />
</label>
<label>
Message
<textarea name="message" rows={5} required />
</label>
<input type="text" name="_gotcha" tabIndex={-1} autoComplete="off" style={{ display: 'none' }} />
<button type="submit" disabled={status === 'sending'}>
{status === 'sending' ? 'Sending...' : 'Send'}
</button>
{status === 'error' && <p role="alert">{error}</p>}
</form>
)
}The inputs are uncontrolled, so a failed submission leaves the visitor's text in place. _gotcha is a honeypot. People never see it, and bots that fill it in are dropped. Because the hook sends FormData, a file input works as well on Pro. Add <input type="file" name="attachment" /> and the file goes with the rest of the fields.
To try it, create a form and swap YOUR_FORM_KEY for the key on its Setup tab. The same tab has a component with your own field names under Code. Wait two seconds before you send a test, because anything faster is treated as a bot.
Errors you can show
A rejected submission comes back with ok: false, an error code and a message written for visitors. These are the ones React apps run into most:
error | Why | Fix |
|---|---|---|
origin_not_allowed | The page isn't in the form's allowed websites | Add the site, and http://localhost:5173 while you develop |
rate_limited | More than 5 submissions a minute from one IP address to one form, 50 on Pro and Business | Wait a minute between tests |
captcha_failed | Turnstile or reCAPTCHA is on and the token is missing | Render the widget inside the form |
quota_exceeded | The team used its monthly submissions | Upgrade, or wait for next month |
The honeypot and the fill-time check answer ok: true on purpose, so bots can't tell they were caught. The error reference lists every code.
Where it runs
The hook uses fetch, FormData and React state, so it works anywhere React does. In a Vite or React Router app, import the component like any other. In Next.js, add 'use client' to the top of the file, as the Next.js page shows. In Astro, render it as an island with client:load. Lovable, Bolt and v0 build React apps too, and the Lovable page has a prompt that makes this change for you.
What you get with Nisuform
Free covers 100 submissions a month across 3 forms. Pro is $10 a month for 5,000 submissions and adds the features marked Pro.
Spam filtering on every plan
A honeypot, a fill-time check, rate limits and duplicate checks run on every submission, with optional Cloudflare Turnstile or Google reCAPTCHA. Spam never counts toward your limit.
An email for every submission
You get an email within seconds of each submission. Reply-to is set to the visitor, so you can answer from your mail app.
Every submission kept
Search, star and export to CSV. Submissions stay until you delete them, so a missed email never loses a message.
File uploadsPro
Up to 5 images or PDFs per submission, stored privately and shared through links that expire.
Slack, Discord, Sheets and webhooksPro
Send each submission to Slack, Discord, Telegram or Google Sheets, or anywhere else with signed webhooks, Zapier or Make.
Auto-repliesPro
Send each visitor a confirmation in your own words, from your own sender name.
React guides
Frequently asked questions
Can a React app send email without a backend?
No. React runs in the browser, and a browser can't send email. Either your own server calls an email provider, or the form posts to a form backend that stores the submission and emails you. With a form backend, the React app stays a static site.
Is it safe to put the form endpoint in my React code?
Yes. The endpoint only accepts new submissions. Nobody can read your submissions or change the form with it, so it doesn't need to be secret. To stop other sites from posting to it, add your domains to the form's allowed websites.
Does it work with React Hook Form, Formik or TanStack Form?
Yes. Let the library handle fields and validation, and send its values to the endpoint from its submit handler with Accept set to application/json. Show an error when the response has ok set to false.
Do I need CORS headers or a proxy?
No. The endpoint accepts requests from any origin, so a React app on localhost, Vercel, Netlify, Cloudflare Pages or GitHub Pages can post to it directly.
How much does it cost?
Free covers 100 submissions a month across 3 forms, with spam filtering, email notifications and the inbox. Pro is $10 a month for 5,000 submissions, unlimited forms, file uploads, webhooks and auto-replies.