Frameworks and libraries
Add a contact form to a React app
React has no opinion about where form data goes, which is why every React tutorial stops at onSubmit. WebForms picks it up from there: POST the form to an endpoint and submissions get stored, spam filtered and routed to email, Slack, a webhook or all three. The examples below cover the uncontrolled FormData approach, controlled state and react-hook-form.
Setup
Grab an endpoint
Create a form in the dashboard and copy its endpoint. No npm install required: this is one fetch call.
Submit the form
The simplest version passes new FormData(e.currentTarget) straight through, so you never write a change handler per field.
Render a success state
Ask for JSON with an accept header and you get { ok: true, id } back, so you can swap in a thank-you message without a page reload.
React examples
Swap in your own endpoint and these run as-is.
Uncontrolled: no state per field, the DOM already holds the values.
import { useState } from "react";
export function ContactForm() {
const [status, setStatus] = useState("idle");
const [error, setError] = useState(null);
async function onSubmit(e) {
e.preventDefault();
setStatus("sending");
const res = await fetch("https://send.webforms.to/wft_your_key", {
method: "POST",
headers: { accept: "application/json" },
body: new FormData(e.currentTarget),
});
const data = await res.json();
if (data.ok) {
setStatus("sent");
} else {
setError(data.message ?? "Something went wrong.");
setStatus("error");
}
}
if (status === "sent") return <p role="status">Thanks, we got it.</p>;
return (
<form onSubmit={onSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<button disabled={status === "sending"}>Send</button>
{error && <p role="alert">{error}</p>}
</form>
);
}react-hook-form, posting JSON instead of FormData.
import { useForm } from "react-hook-form";
type Values = { name: string; email: string; message: string };
export function ContactForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting, isSubmitSuccessful },
} = useForm<Values>();
async function onSubmit(values: Values) {
const res = await fetch("https://send.webforms.to/wft_your_key", {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json",
},
body: JSON.stringify(values),
});
if (!res.ok) throw new Error("Submission failed");
}
if (isSubmitSuccessful) return <p role="status">Thanks, we got it.</p>;
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name", { required: "Name is required" })} />
{errors.name && <span role="alert">{errors.name.message}</span>}
<input type="email" {...register("email", { required: true })} />
<textarea {...register("message", { required: true })} />
<button disabled={isSubmitting}>Send</button>
</form>
);
}Worth knowing
- FormData sends multipart, JSON sends JSON. Both are accepted. Use FormData when you want checkbox groups to collapse into arrays automatically.
- Any field whose name starts with an underscore is treated as a directive, not data. _gotcha, _subject, _replyto and _next are the useful ones.
React form FAQ
No. The browser posts directly to your endpoint. That is the whole integration: one fetch call, no backend to deploy or maintain.
Either. Uncontrolled with new FormData(e.currentTarget) is less code and fewer re-renders. Controlled state is worth it when fields depend on each other.
Yes. They own validation and UI state; WebForms only cares about the HTTP POST at the end. Send the values object as JSON, as in the second example.
Related guides
Where React submissions can go
The guide above gets the submission to your endpoint. From there it can land anywhere: one destination on Free, several on Pro.
Need the form markup too? The template gallery has complete forms for contact, waitlist, job application and more, and the form generator builds one field by field.
Ready to wire up your React form?
Free for 300 submissions a month. Endpoint in under a minute, no card required.
Start free