React Router forms
without email plumbing.
Use actions, useNavigation, or useFetcher for the UI while Form Plume handles email notifications, stored submissions, spam filtering, uploads, integrations, and webhooks.
export async function action({ request }) {
const formData = await request.formData();
const response = await fetch(FORM_PLUME_ENDPOINT, {
method: "POST",
body: formData,
});
if (!response.ok) {
return { error: "Please try again." };
}
return redirect("/thanks");
}The full guide
A hosted backend for React Router actions.
Let React Router own navigation and pending state while Form Plume owns delivery, storage, spam filtering, and downstream workflows.
- 1Create your Form Plume accountStart free and give your React Router form a hosted endpoint for email, submissions, spam filtering, uploads, integrations, and webhooks.Start free
- 2Create the form and copy the endpointCopy the endpoint URL from Form Plume. It looks like
https://api.formplume.com/f/your-slug - 3Connect it inside React RouterUse a framework route action or data-router action to receive FormData, forward it to Form Plume, and return an accessible result. Send one test submission from the real React Router app or preview environment and confirm it reaches Form Plume.
You can use this AI prompt to hand the platform-specific wiring to an assistant without accidentally creating a backend.
React Router framework route
In React Router framework mode, a contact page can export an action. The action receives the request, reads FormData, forwards it to Form Plume, and returns either an error or a redirect.
import {
Form,
redirect,
useActionData,
useNavigation,
type ActionFunctionArgs,
} from "react-router";
const FORM_PLUME_ENDPOINT = "PASTE_YOUR_FORM_PLUME_ENDPOINT_HERE";
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const response = await fetch(FORM_PLUME_ENDPOINT, {
method: "POST",
body: formData,
});
if (!response.ok) {
return { error: "We could not send your message. Please try again." };
}
return redirect("/thanks");
}
export default function ContactRoute() {
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const sending = navigation.state === "submitting";
return (
<Form method="post">
<label htmlFor="name">Name</label>
<input id="name" name="name" autoComplete="name" required />
<label htmlFor="email">Email</label>
<input id="email" type="email" name="email" autoComplete="email" required />
<label htmlFor="message">Message</label>
<textarea id="message" name="message" rows={5} required />
<button type="submit" disabled={sending}>
{sending ? "Sending..." : "Send message"}
</button>
{actionData?.error && <p role="alert">{actionData.error}</p>}
</Form>
);
}This keeps React Router responsible for navigation and form state while Form Plume handles email notifications, dashboard storage, spam filtering, uploads, integrations, and webhooks.
Data router forms
If your app uses a data router instead of file routes, the same idea applies. Define an action for the route object and post to it with <Form method="post">.
{
path: "/contact",
element: <ContactPage />,
action: async ({ request }) => {
const formData = await request.formData();
const response = await fetch(FORM_PLUME_ENDPOINT, {
method: "POST",
body: formData,
});
if (!response.ok) {
return { error: "Please try again." };
}
return redirect("/thanks");
},
}Use useFetcher when the form should submit without navigating away from the current page.
Uploads
File uploads work when the form uses multipart encoding and the action forwards the original FormData.
<Form method="post" encType="multipart/form-data">
<input type="file" name="attachment" accept=".pdf,.png,.jpg" />
<button type="submit">Send message</button>
</Form>Do not convert the body to JSON when the form includes files.
Validation and errors
React Router actions are a good place for required-field checks, allowlists, and user-facing errors. Return a small error object when the visitor should fix the form, then let Form Plume handle delivery once the payload is valid.
Keep field names simple: name, email, message, company, phone, subject, page, source, and attachment are easy to route in email and integrations.
After Form Plume receives it
Once the action forwards the submission, Form Plume can notify email, store the message, quarantine suspicious spam, and trigger Slack, Discord, Google Sheets, Mailchimp, ActiveCampaign, or a signed webhook.
Primary sources
FAQ
React Router questions
before launch.
One line. Zero backend.
