React
Client componentOne hook handles the request, the pending state and the error message. It works with plain state, React Hook Form or any form library.
The hook
Keep the endpoint in one module so a form is never hard-coded twice. The hook returns a small status machine instead of several booleans.
use-formivo-submit.ts
import { useCallback, useState } from "react";
const ENDPOINT = "https://formivo.faizyab.me/api/f/YOUR_FORM_ID";
type Status = "idle" | "submitting" | "success" | "error";
export function useFormivoSubmit() {
const [status, setStatus] = useState<Status>("idle");
const [error, setError] = useState<string | null>(null);
const submit = useCallback(async (values: Record<string, unknown>) => {
setStatus("submitting");
setError(null);
try {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify(values),
});
const result = await response.json().catch(() => null);
if (!response.ok || !result?.success) {
throw new Error(result?.error?.message ?? "The form could not be sent.");
}
setStatus("success");
return result as { leadId: string };
} catch (cause) {
setStatus("error");
setError(cause instanceof Error ? cause.message : "The form could not be sent.");
return null;
}
}, []);
return { status, error, submit };
}Use it in a form
ContactForm.tsx
import { useFormivoSubmit } from "./use-formivo-submit";
export function ContactForm() {
const { status, error, submit } = useFormivoSubmit();
async function handleSubmit(event) {
event.preventDefault();
const data = new FormData(event.currentTarget);
const result = await submit(Object.fromEntries(data.entries()));
if (result) event.currentTarget.reset();
}
return (
<form onSubmit={handleSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" />
<button type="submit" disabled={status === "submitting"}>
{status === "submitting" ? "Sending…" : "Send"}
</button>
{status === "success" ? <p role="status">Thanks, we received your message.</p> : null}
{error ? <p role="alert">{error}</p> : null}
</form>
);
}Announce success with role="status" and failures with role="alert" so screen readers hear the outcome without a page reload.
With a form library
The hook does not own your inputs, so React Hook Form, TanStack Form and Formik all work. Hand it the validated values:
ContactForm.tsx
import { useForm } from "react-hook-form";
export function ContactForm() {
const { register, handleSubmit, reset, formState } = useForm();
const { status, error, submit } = useFormivoSubmit();
return (
<form
onSubmit={handleSubmit(async (values) => {
const result = await submit(values);
if (result) reset();
})}
>
<input {...register("name", { required: true })} />
<input type="email" {...register("email", { required: true })} />
<textarea {...register("message")} />
<button type="submit" disabled={formState.isSubmitting}>
Send
</button>
{error ? <p role="alert">{error}</p> : null}
</form>
);
}Native validation still counts
Client-side validation improves the experience but is never the security boundary. Formivo validates every payload server-side, so a request that skips your form still has to pass the endpoint's rules.