Files
gh-jezweb-claude-skills-ski…/references/react-integration.md
2025-11-30 08:24:31 +08:00

11 KiB

React Integration Guide

Best practices for integrating Turnstile with React, Next.js, and modern React patterns

Recommended Package: @marsidev/react-turnstile (Cloudflare-verified)


Package Installation

npm install @marsidev/react-turnstile
# or
pnpm add @marsidev/react-turnstile
# or
yarn add @marsidev/react-turnstile

Current Version: 1.3.1 (September 2025) React Compatibility: React 18+, Next.js 13+, 14+, 15+


Basic Usage

import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'

export function ContactForm() {
  const [token, setToken] = useState<string>()

  return (
    <form>
      <input name="email" type="email" required />
      <textarea name="message" required />

      <Turnstile
        siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
        onSuccess={setToken}
      />

      <button disabled={!token}>Submit</button>
    </form>
  )
}

Props Reference

Required Props

siteKey

Type: string Description: Your Turnstile sitekey

<Turnstile siteKey="YOUR_SITE_KEY" />

Optional Props

onSuccess

Type: (token: string) => void Description: Called when challenge succeeds

<Turnstile onSuccess={(token) => console.log(token)} />

onError

Type: (error: string) => void Description: Called when challenge fails

<Turnstile onError={(error) => console.error(error)} />

onExpire

Type: () => void Description: Called when token expires (5 min)

<Turnstile onExpire={() => setToken(undefined)} />

onAbort

Type: () => void Description: Called when challenge is aborted

options

Type: TurnstileOptions Description: Widget configuration

<Turnstile
  siteKey="..."
  options={{
    theme: 'dark',
    size: 'compact',
    action: 'login',
  }}
/>

Using Refs

Access widget instance for manual control:

import { useRef } from 'react'
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'

export function AdvancedForm() {
  const turnstileRef = useRef<TurnstileInstance>(null)

  function handleReset() {
    turnstileRef.current?.reset()
  }

  function handleRemove() {
    turnstileRef.current?.remove()
  }

  return (
    <>
      <Turnstile ref={turnstileRef} siteKey="..." />
      <button onClick={handleReset}>Reset</button>
      <button onClick={handleRemove}>Remove</button>
    </>
  )
}

Next.js App Router

Client Component

// app/contact/page.tsx
'use client'

import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'

export default function ContactPage() {
  const [token, setToken] = useState<string>()

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()

    const formData = new FormData(e.currentTarget)

    const response = await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: formData.get('email'),
        message: formData.get('message'),
        'cf-turnstile-response': token,
      }),
    })

    if (response.ok) {
      alert('Success!')
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <textarea name="message" required />

      <Turnstile
        siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
        onSuccess={setToken}
        onExpire={() => setToken(undefined)}
      />

      <button disabled={!token}>Submit</button>
    </form>
  )
}

Server Action (Next.js 14+)

'use server'

import { validateTurnstile } from '@/lib/turnstile'

export async function submitContact(formData: FormData) {
  const token = formData.get('cf-turnstile-response')?.toString()

  if (!token) {
    return { error: 'Missing verification' }
  }

  const result = await validateTurnstile(token, process.env.TURNSTILE_SECRET_KEY!)

  if (!result.success) {
    return { error: 'Verification failed' }
  }

  // Process form
  return { success: true }
}

Next.js Pages Router

Page Component

// pages/contact.tsx
import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'

export default function ContactPage() {
  const [token, setToken] = useState<string>()

  return (
    <form>
      <Turnstile
        siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
        onSuccess={setToken}
      />
    </form>
  )
}

API Route

// pages/api/contact.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { validateTurnstile } from '@/lib/turnstile'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  const token = req.body['cf-turnstile-response']

  if (!token) {
    return res.status(400).json({ error: 'Missing token' })
  }

  const result = await validateTurnstile(
    token,
    process.env.TURNSTILE_SECRET_KEY!,
    {
      remoteip: req.headers['x-forwarded-for']?.toString() || req.socket.remoteAddress,
    }
  )

  if (!result.success) {
    return res.status(401).json({ error: 'Invalid token' })
  }

  // Process form
  res.status(200).json({ success: true })
}

Custom Hook Pattern

// hooks/useTurnstile.ts
import { useRef, useState } from 'react'
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'

export function useTurnstile(siteKey: string) {
  const [token, setToken] = useState<string>()
  const [isReady, setIsReady] = useState(false)
  const [error, setError] = useState<string>()
  const turnstileRef = useRef<TurnstileInstance>(null)

  const reset = () => {
    turnstileRef.current?.reset()
    setToken(undefined)
    setIsReady(false)
    setError(undefined)
  }

  const TurnstileWidget = () => (
    <Turnstile
      ref={turnstileRef}
      siteKey={siteKey}
      onSuccess={(token) => {
        setToken(token)
        setIsReady(true)
        setError(undefined)
      }}
      onError={(err) => {
        setError(err)
        setIsReady(false)
      }}
      onExpire={() => {
        setToken(undefined)
        setIsReady(false)
      }}
    />
  )

  return {
    token,
    isReady,
    error,
    reset,
    TurnstileWidget,
  }
}

// Usage
export function MyForm() {
  const { token, isReady, error, reset, TurnstileWidget } = useTurnstile(
    process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!
  )

  return (
    <form>
      <TurnstileWidget />
      {error && <div>Error: {error}</div>}
      <button disabled={!isReady}>Submit</button>
    </form>
  )
}

Jest Testing

Mock Setup

// jest.setup.ts
import React from 'react'

jest.mock('@marsidev/react-turnstile', () => ({
  Turnstile: ({ onSuccess }: { onSuccess: (token: string) => void }) => {
    React.useEffect(() => {
      onSuccess('XXXX.DUMMY.TOKEN.XXXX')
    }, [onSuccess])

    return <div data-testid="turnstile-mock" />
  },
}))

Component Test

// ContactForm.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { ContactForm } from './ContactForm'

test('submits form with Turnstile', async () => {
  render(<ContactForm />)

  const submitButton = screen.getByRole('button', { name: 'Submit' })

  // Turnstile auto-solves (mocked)
  await waitFor(() => {
    expect(submitButton).not.toBeDisabled()
  })

  fireEvent.click(submitButton)

  expect(await screen.findByText('Success')).toBeInTheDocument()
})

Environment-Aware Sitekey

// lib/turnstile.ts
export function useTurnstileSiteKey() {
  // Test/Development: Use dummy key
  if (process.env.NODE_ENV !== 'production') {
    return '1x00000000000000000000AA'
  }

  // Production: Use real key
  return process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!
}

// Usage
import { useTurnstileSiteKey } from '@/lib/turnstile'

export function MyForm() {
  const siteKey = useTurnstileSiteKey()

  return <Turnstile siteKey={siteKey} />
}

Known Issues & Workarounds

Issue #112: Next.js + Jest Compatibility (Oct 2025)

Problem: @marsidev/react-turnstile breaks Jest tests

Source: https://github.com/marsidev/react-turnstile/issues/112

Workaround: Mock the component (see Jest Testing section above)

Issue #113: Blocked Script Execution (Oct 2025)

Problem: Script execution blocked in some environments

Source: https://github.com/marsidev/react-turnstile/issues/113

Workaround:

  1. Check CSP headers allow challenges.cloudflare.com
  2. Ensure api.js loads from CDN (not proxied)

TypeScript Types

import type {
  TurnstileInstance,
  TurnstileProps,
  TurnstileOptions,
} from '@marsidev/react-turnstile'

// Widget instance methods
interface TurnstileInstance {
  reset(): void
  remove(): void
  execute(): void
  getResponse(): string | undefined
  isExpired(): boolean
}

// Component props
interface TurnstileProps {
  siteKey: string
  onSuccess?: (token: string) => void
  onError?: (error: string) => void
  onExpire?: () => void
  onAbort?: () => void
  options?: TurnstileOptions
  scriptOptions?: {
    nonce?: string
    defer?: boolean
    async?: boolean
  }
}

// Widget options
interface TurnstileOptions {
  theme?: 'light' | 'dark' | 'auto'
  size?: 'normal' | 'compact' | 'flexible'
  action?: string
  cdata?: string
  execution?: 'render' | 'execute'
  appearance?: 'always' | 'execute' | 'interaction-only'
  retry?: 'auto' | 'never'
  'retry-interval'?: number
}

Performance Optimization

Lazy Loading

import { lazy, Suspense } from 'react'

const Turnstile = lazy(() =>
  import('@marsidev/react-turnstile').then(mod => ({ default: mod.Turnstile }))
)

export function LazyTurnstileForm() {
  return (
    <form>
      <Suspense fallback={<div>Loading verification...</div>}>
        <Turnstile siteKey="..." />
      </Suspense>
    </form>
  )
}

Conditional Rendering

Only render Turnstile when needed:

export function ConditionalForm() {
  const [showTurnstile, setShowTurnstile] = useState(false)

  return (
    <form>
      <input onChange={() => setShowTurnstile(true)} />

      {showTurnstile && <Turnstile siteKey="..." />}
    </form>
  )
}

Best Practices

Use environment variables for sitekeys Mock in tests using Jest setup file Handle expiration with onExpire callback Disable submit until ready based on token state Reset after submission for multi-use forms Use TypeScript for type safety Lazy load if not immediately needed

Don't hardcode sitekeys in components Don't skip error handling (onError) Don't forget server validation (critical!) Don't use production keys in tests


Last Updated: 2025-10-22 Package Version: @marsidev/react-turnstile@1.3.1 Cloudflare Status: Officially Recommended