React
A React form with pending, error and success states, field errors and an optional human check.
This works in any React app: Vite, Remix, React Router, Gatsby or a Next.js client component. It posts straight from the browser, so each visitor counts toward their own rate limit.
Install the SDK
npm install @ikiform/sdkA complete form
import { challenge, submit } from "@ikiform/sdk";
import { useState } from "react";
const FORM_ID = "frm_xxxxxxxxxxxxxxxx";
export function ContactForm({ humanCheck = false }) {
const [status, setStatus] = useState("idle");
const [errors, setErrors] = useState({});
const [message, setMessage] = useState("");
async function onSubmit(event) {
event.preventDefault();
const data = new FormData(event.currentTarget);
setStatus("sending");
setErrors({});
try {
const turnstileToken = humanCheck ? await challenge(FORM_ID) : undefined;
const result = await submit(FORM_ID, data, { turnstileToken });
if (result.ok) return setStatus("sent");
setErrors(result.error.fields ?? {});
setMessage(result.error.message);
} catch {
setMessage("The human check didn't finish. Try again.");
}
setStatus("error");
}
if (status === "sent") return <p>Thanks, we got it.</p>;
return (
<form onSubmit={onSubmit}>
<label>
Email
<input name="email" type="email" required />
</label>
{errors.email ? <p>{errors.email[0]}</p> : null}
<label>
Message
<textarea name="message" />
</label>
{errors.message ? <p>{errors.message[0]}</p> : null}
<input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
{status === "error" && message ? <p role="alert">{message}</p> : null}
<button disabled={status === "sending"}>Send</button>
</form>
);
}- Pass
humanCheckwhen the form's spam check is set to Ikiform. With the check off, leave it out. challengerejects with aChallengeErrorif the check fails or is aborted, which thecatchturns into a message.- Without the SDK, replace
submitwithfetchas on JavaScript and fetch.
As a form action
In React 19, the value submit returns also satisfies the form action type, so it can be passed straight to action. React's internal $ACTION_ fields are dropped before sending.
import { submit } from "@ikiform/sdk";
export function Newsletter() {
return (
<form action={(data) => submit("frm_xxxxxxxxxxxxxxxx", data)}>
<input name="email" type="email" required />
<button>Subscribe</button>
</form>
);
}Validate before sending
validate uses the same rules and messages as the server for all 12 field types, so you can show errors before the request. The server stays the source of truth.
import { getSchema, validate } from "@ikiform/sdk";
const schema = await getSchema("frm_xxxxxxxxxxxxxxxx");
function check(form) {
if (!schema.ok) return {};
return validate(schema.schema.fields, new FormData(form));
}