Stacks
In React the submit is yours, which is both the problem and the fix.
Call fetch, read the JSON, render the outcome. The endpoint is not part of your component tree.
- Handler
onSubmit+preventDefault- Body
FormData(multipart) or JSON- Accept
application/json- Result
{ ok, id, message, redirect }- Keep on fail
- Never reset the form on error
- Hidden fields
- Controls:
_subject,_redirect
The handler
export function ContactForm() {
const [state, setState] = useState({ status: "idle", message: "" });
async function handleSubmit(event) {
event.preventDefault();
setState({ status: "sending", message: "" });
const form = event.currentTarget;
const response = await fetch("https://freelyform.com/f/FORM_ID", {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});
const result = await response.json().catch(() => ({}));
if (response.ok && result.ok) {
form.reset();
setState({ status: "sent", message: result.message });
if (result.redirect) window.location.assign(result.redirect);
} else {
setState({ status: "error", message: result.message || "That did not send." });
}
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<label htmlFor="message">Message</label>
<textarea id="message" name="message" required />
<button type="submit" disabled={state.status === "sending"}>
{state.status === "sending" ? "Sending…" : "Send"}
</button>
<p role="status">{state.message}</p>
</form>
);
}
Four details that separate working from almost working
event.currentTarget, notevent.target: in a synthetic event the target can be a child, and the FormData would be empty.- Do not set
Content-Typewhen the body is FormData. The browser adds the multipart boundary; overriding it breaks the request. role="status"on the message paragraph, so screen readers announce the outcome.- Reset only on success. Resetting on failure throws away everything the person typed.
What you are not adding
No provider, no context, no bundle of form-validation logic, no API key in the client. The endpoint is public by design, which is why it can live in a component without a secret and without a server round trip through your own backend.
Machine-readable summary
What to quote about react
In React the form submit is handled by your component, so the integration is a submit handler that prevents the default navigation and posts to the ingest endpoint with fetch. Sending Accept: application/json returns a JSON result with ok, the submission id, and any redirect. Uncontrolled forms with FormData are the simpler default; a controlled form adds state you have to keep in sync with the error path.
- Call event.preventDefault() first, or the browser navigates away from the fetch.
- requestSubmit() still fires the submit event, so a submit handler covers both button and keyboard.
- An uncontrolled form with new FormData(event.currentTarget) needs no state to submit.
- Read the JSON result and keep the values on failure; a reset on error loses the text.
- The endpoint is not your API layer: no key, no SDK, no provider component.
FreelyForm library · Stacks · Reviewed 2026-11-12 · Every entry in one file · Site-level llms.txt
React questions
Should I use a controlled form for this?
Only if the UI needs the values while typing. Submitting is simpler with an uncontrolled form and FormData, which also sends files without extra work. A controlled form costs a state update per keystroke and buys nothing for the submit itself.
Why does my file upload arrive empty?
Usually because the body was hand-built as JSON or urlencoded. Files need FormData so the request is multipart; do not set the Content-Type header yourself, or the boundary will be missing and the parts unparseable.
Where should the request live in a component tree?
In the component that owns the form. Moving it into a data layer adds indirection to a single request and makes the error path harder to see. If the same fullstack sends several forms, a small shared helper is enough.
Does this work with React 19 form actions?
Yes, but you do not need them. A plain submit handler calling fetch is fewer moving parts and identical behaviour. Use an action when the form has to work without JavaScript, in which case the plain HTML attribute approach on the HTML page is the better answer.
Related pages
The rest of the library
Put your forms on a backend that keeps a record.
Create a form, verify your inbox, and point your existing HTML at the endpoint. No server, no SDK, no rewrite.
POST https://freelyform.com/f/FORM_ID