react-turnstile

A minimal React component and hook for rendering Cloudflare Turnstile CAPTCHA challenges.

SDK
npm
v1.1.5
157stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
38/100Needs Attention
Development Activity4
Maintenance20
Community56
Maturity52
Momentum20

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
50/100Fair
Architecture68
Code Quality35
Innovation60
Learning Curve35

react-turnstile is a lightweight wrapper around Cloudflare’s Turnstile CAPTCHA-alternative widget, giving React developers a declarative <Turnstile /> component instead of hand-rolling script loading and DOM management. It lazy-loads Cloudflare’s script once per page, uses explicit rendering to avoid duplicate widgets, and exposes every Turnstile callback (onVerify, onSuccess, onLoad, onError, onExpire, onTimeout, and more) as typed React props.

Beyond the component, the library ships a useTurnstile() hook and a “bound turnstile object” passed to every callback, so consumers can call execute, reset, getResponse, and isExpired against a specific widget instance without manually tracking the underlying widgetId. A fixedSize prop pre-allocates layout space to avoid cumulative layout shift while the widget loads asynchronously.

What You Get

  • Declarative <Turnstile /> component with typed props for every Turnstile configuration option (sitekey, theme, language, size, appearance, execution mode)
  • Full callback coverage — onVerify, onSuccess, onLoad, onError, onExpire, onTimeout, onAfterInteractive, onBeforeInteractive, onUnsupported — each receiving a bound turnstile object
  • useTurnstile() hook for accessing the global Turnstile object once the script has finished loading
  • fixedSize prop that reserves the widget’s final width/height up front to prevent layout shift during async load

Common Use Cases

  • Gating a login or signup form behind a Turnstile challenge before submitting credentials to the backend
  • Protecting a public contact or comment form from spam bots without a traditional CAPTCHA’s UX friction
  • Invisible or interaction-only verification on checkout flows where visible challenges would hurt conversion
  • Manually resetting or re-executing a challenge after a failed server-side token verification

Under The Hood

Architecture The entire implementation lives in a single module (src/index.tsx, ~280 lines) built around a module-level singleton state machine (unloaded/loading/ready) that lazy-loads Cloudflare’s external script exactly once per page via a shared promise. The <Turnstile> component wires that promise into a useEffect that imperatively calls window.turnstile.render and tears the widget down on unmount or dependency change, while a mutable “in-place state” object (seeded via useState’s initial value but written to directly) sidesteps stale-closure issues inside the render effect’s captured callbacks. A second, separate useEffect keeps that mutable object’s callback references fresh across re-renders without re-triggering the widget-render effect, avoiding needless widget remounts on every prop change. createBoundTurnstileObject() wraps a widgetId into a small object exposing execute/reset/getResponse/isExpired so callers never track the id themselves. The library has no abstraction layer between itself and Cloudflare’s window.turnstile API — if that external surface changes shape, the wrapper breaks directly.

Tech Stack Written entirely in TypeScript and compiled with a plain tsc build (no bundler such as esbuild or Rollup). Runtime dependencies are limited to peer dependencies on react and react-dom (>=16.13.1); the only devDependency of note is turnstile-types, supplying the upstream TS types (RenderParameters, SupportedLanguages, TurnstileObject) that the component’s props are built on. Output is published as a dual ESM/CJS package via the exports map, pointing at a single dist/index.js plus dist/index.d.ts. The Cloudflare Turnstile script itself is loaded at runtime from challenges.cloudflare.com, not bundled into the package.

Code Quality No test files, test runner, or CI configuration are present in the repository — the library is untested beyond manual and community use. Error handling is minimal: a failed script load rejects a shared promise with a plain string (“Failed to load Turnstile.”) rather than a typed Error, and that rejection is funneled into the onError callback. TypeScript typing is thorough for the public API surface (dedicated TurnstileProps, TurnstileCallbacks, BoundTurnstileObject interfaces sourced from turnstile-types), though the module-level global namespace reference is typed as any. No ESLint configuration exists; only Prettier is wired up for formatting. Naming is consistent and idiomatic throughout.

API Design The public surface is intentionally narrow — one component plus one hook — with every Turnstile option exposed as an individually typed, flat prop rather than a single opaque options object. The “bound turnstile object” pattern scopes execute/reset/getResponse/isExpired to a specific widget instance, removing the common footgun of a multi-widget page accidentally operating on the wrong challenge. The fixedSize prop collapses a well-known real pain point — layout shift from the widget’s asynchronously loaded iframe — into a single boolean. Getting started requires nothing more than <Turnstile sitekey="..." onVerify={...} />. The overall approach is not novel — several comparable Turnstile wrappers exist across ecosystems, including a competing package already listed in this catalog — but the ergonomics are clean for a narrow-purpose glue library.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search