How to Send Emails from React (2026 Guide)

React runs in the browser. You can't send emails directly from the client because that would expose your API keys. Instead, you call a backend API that sends the email server-side.
This guide covers the React side (forms, fetch calls, loading states) and the minimal backend needed to actually send.
The Pattern
React Form → fetch("/api/send-email") → Your Backend → Email Provider
Your React app sends a POST request to your own backend. The backend holds the API key and calls the email provider.
React Contact Form
// src/components/ContactForm.tsx
import { useState, FormEvent } from "react";
export function ContactForm() {
const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle");
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus("sending");
const form = new FormData(e.currentTarget);
try {
const res = await fetch("/api/send-email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: form.get("email"),
message: form.get("message"),
}),
});
if (!res.ok) throw new Error("Failed");
setStatus("sent");
} catch {
setStatus("error");
}
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" placeholder="Your email" required />
<textarea name="message" placeholder="Message" required />
<button type="submit" disabled={status === "sending"}>
{status === "sending" ? "Sending..." : "Send"}
</button>
{status === "sent" && <p style={{ color: "green" }}>Message sent!</p>}
{status === "error" && <p style={{ color: "red" }}>Failed to send. Try again.</p>}
</form>
);
}Backend API
Your backend handles the actual email sending. Here's a minimal Express server:
import express from "express";
import Sequenzy from "sequenzy";
const app = express();
const sequenzy = new Sequenzy();
app.use(express.json());
app.post("/api/send-email", async (req, res) => {
const { email, message } = req.body;
if (!email || !message) {
return res.status(400).json({ error: "email and message required" });
}
try {
await sequenzy.transactional.send({
to: "you@yourcompany.com",
subject: `Contact from ${email}`,
body: `<p><strong>From:</strong> ${email}</p><p>${message}</p>`,
});
res.json({ sent: true });
} catch {
res.status(500).json({ error: "Failed to send" });
}
});
app.listen(3001);import express from "express";
import { Resend } from "resend";
const app = express();
const resend = new Resend(process.env.RESEND_API_KEY);
app.use(express.json());
app.post("/api/send-email", async (req, res) => {
const { email, message } = req.body;
if (!email || !message) {
return res.status(400).json({ error: "email and message required" });
}
const { error } = await resend.emails.send({
from: "Contact <noreply@yourdomain.com>",
to: "you@yourcompany.com",
subject: `Contact from ${email}`,
html: `<p><strong>From:</strong> ${email}</p><p>${message}</p>`,
});
if (error) {
return res.status(500).json({ error: "Failed to send" });
}
res.json({ sent: true });
});
app.listen(3001);import express from "express";
import sgMail from "@sendgrid/mail";
sgMail.setApiKey(process.env.SENDGRID_API_KEY!);
const app = express();
app.use(express.json());
app.post("/api/send-email", async (req, res) => {
const { email, message } = req.body;
if (!email || !message) {
return res.status(400).json({ error: "email and message required" });
}
try {
await sgMail.send({
to: "you@yourcompany.com",
from: "noreply@yourdomain.com",
subject: `Contact from ${email}`,
html: `<p><strong>From:</strong> ${email}</p><p>${message}</p>`,
});
res.json({ sent: true });
} catch {
res.status(500).json({ error: "Failed to send" });
}
});
app.listen(3001);With a Custom Hook
// src/hooks/useSendEmail.ts
import { useState } from "react";
interface SendEmailInput {
email: string;
message: string;
}
export function useSendEmail() {
const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle");
async function send(data: SendEmailInput) {
setStatus("sending");
try {
const res = await fetch("/api/send-email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error("Failed");
setStatus("sent");
} catch {
setStatus("error");
}
}
return { send, status, isSending: status === "sending" };
}
// Usage:
// const { send, isSending, status } = useSendEmail();
// await send({ email: "user@example.com", message: "Hello" });With React Router (Remix-style)
If you're using React Router v7 with server functions:
// app/routes/contact.tsx
import { Form, useActionData, useNavigation } from "react-router";
export async function action({ request }: { request: Request }) {
const formData = await request.formData();
const email = formData.get("email") as string;
const message = formData.get("message") as string;
// Send email server-side (same as Express examples above)
await fetch("https://api.sequenzy.com/v1/transactional/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SEQUENZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "you@yourcompany.com",
subject: `Contact from ${email}`,
body: `<p>${message}</p>`,
}),
});
return { sent: true };
}
export default function Contact() {
const data = useActionData<typeof action>();
const navigation = useNavigation();
return (
<Form method="post">
<input name="email" type="email" required />
<textarea name="message" required />
<button disabled={navigation.state === "submitting"}>Send</button>
{data?.sent && <p>Sent!</p>}
</Form>
);
}Going to Production
1. Never Expose API Keys
API keys must stay on the server. Never put them in React code or environment variables prefixed with VITE_ or NEXT_PUBLIC_.
2. Rate Limit Your Backend
Add rate limiting to your email endpoint to prevent abuse.
3. Verify Your Domain
Add SPF, DKIM, DMARC DNS records with your email provider.
Beyond Transactional
Sequenzy handles transactional sends, marketing campaigns, automated sequences, and subscriber management from one API. Native Stripe integration for SaaS.
Wrapping Up
- React forms with loading states and error handling
- Backend API to keep API keys secure
- Custom hooks for reusable email sending logic
- React Router for server-side form actions
Pick your provider, build the backend, and connect your React forms.