NEXAFORGE.STUDIO

IMPLEMENTATION GUIDE · REACT FORMS

How to Protect React Forms with Cloudflare Turnstile

A React integration is secure only when the backend validates the token before processing the form. The component collects a short-lived token; your API makes the security decision.

Understand the four-step request flow

First, the React page renders Turnstile with the public site key. Second, the successful widget callback gives the browser a token. Third, the form sends that token and the form fields to your API. Fourth, the API calls Siteverify with the secret key and accepts the business request only when validation succeeds.

The browser must never receive the secret key. A disabled submit button is user experience, not the security boundary; the API must reject a missing or invalid token.

Keep widget lifecycle and form state separate

Store the Turnstile token in component state and clear it after every submission attempt that consumes the token. Reset the widget after an expired token or a server response that requires a fresh challenge.

React Strict Mode and route changes can cause effects to run more than once during development. Use the integration library's lifecycle support or an explicit container reference so duplicate widgets are not appended.

const [token, setToken] = useState("");

<Turnstile
  sitekey={import.meta.env.VITE_TURNSTILE_SITE_KEY}
  onVerify={setToken}
  onExpire={() => setToken("")}
  onError={() => setToken("")}
/>

<button disabled={!token || pending}>Send</button>

Send the token in the same protected request

Include the token with the fields the API is about to process. Do not validate in a separate browser request and then trust a client-side flag; that creates a gap between validation and the protected action.

For a contact form, the backend should validate before sending email. For registration, validate before creating the account. For checkout, place validation before the expensive or abuse-sensitive step defined by the application's risk model.

await fetch("/api/contact", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name, email, message, turnstileToken: token }),
});

Make Siteverify the backend gate

The API posts the secret and response token to Siteverify. Treat a network error, malformed response or success:false as a failed validation unless your risk model explicitly defines another behavior.

Where configured, compare hostname and action to the expected form. This prevents a valid token from another widget or flow being accepted blindly.

const body = new URLSearchParams({
  secret: process.env.TURNSTILE_SECRET_KEY!,
  response: turnstileToken,
});
const result = await fetch(
  "https://challenges.cloudflare.com/turnstile/v0/siteverify",
  { method: "POST", body }
).then((response) => response.json());
if (!result.success) return new Response("Verification failed", { status: 400 });

Handle expiry, retries and double submissions

Turnstile tokens expire after five minutes and are single-use. A user who waits on the page or retries after the backend already consumed the token needs a new challenge. Map timeout-or-duplicate to a clear retry message rather than a generic form failure.

Disable repeated submissions while the request is pending, but keep server idempotency for actions where duplicate processing would be harmful. Turnstile token rules do not replace application idempotency.

Production testing checklist

Use Cloudflare's documented testing keys in automated or non-production scenarios and real environment-specific keys for the deployed hostname. Verify the direct API path as well as the visible form.

  • ▸ Missing token is rejected
  • ▸ Invalid and expired tokens are rejected
  • ▸ A used token cannot be replayed
  • ▸ The production hostname is allowed
  • ▸ CSP permits required Turnstile resources
  • ▸ Keyboard, mobile and slow-network states remain usable

Frequently asked questions

Can I verify Turnstile only in React?

No. React receives the token, but your backend must validate it with Siteverify before processing the protected request.

Why does the token fail after the user waits?

Turnstile tokens expire after 300 seconds and can be used only once. Reset the widget and request a fresh token when the form is submitted late or retried.

Should the Turnstile secret be in a VITE environment variable?

No. Variables exposed to Vite client code can be delivered to the browser. Only the site key is public; keep the secret in the backend environment.

Related service and guides

Need production help? Email [email protected] or contact @taoquan8 on Telegram.