Skip to main content
New:Build with AI agents

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.

Free foreverNo credit cardReact Router endpoint ready in under a minute
routes/contact.tsx
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.

  1. 1
    Create 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
  2. 2
    Create the form and copy the endpointCopy the endpoint URL from Form Plume. It looks like https://api.formplume.com/f/your-slug
  3. 3
    Connect 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.

Use a framework route action or data-router action when the form should participate in navigation, validation, and redirects. Use useFetcher when the form should submit without leaving the current route.

One line. Zero backend.

The form backend you don’t have to build.

Free foreverNo credit cardSet up in under a minute