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, _cc, _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
Ready to wire up your React form?
Free for 300 submissions a month. Endpoint in under a minute, no card required.
Start free