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

551 lines
11 KiB
Markdown

# 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
```bash
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
```tsx
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
```tsx
<Turnstile siteKey="YOUR_SITE_KEY" />
```
### Optional Props
#### `onSuccess`
**Type**: `(token: string) => void`
**Description**: Called when challenge succeeds
```tsx
<Turnstile onSuccess={(token) => console.log(token)} />
```
#### `onError`
**Type**: `(error: string) => void`
**Description**: Called when challenge fails
```tsx
<Turnstile onError={(error) => console.error(error)} />
```
#### `onExpire`
**Type**: `() => void`
**Description**: Called when token expires (5 min)
```tsx
<Turnstile onExpire={() => setToken(undefined)} />
```
#### `onAbort`
**Type**: `() => void`
**Description**: Called when challenge is aborted
#### `options`
**Type**: `TurnstileOptions`
**Description**: Widget configuration
```tsx
<Turnstile
siteKey="..."
options={{
theme: 'dark',
size: 'compact',
action: 'login',
}}
/>
```
---
## Using Refs
Access widget instance for manual control:
```tsx
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
```tsx
// 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+)
```tsx
'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
```tsx
// 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
```typescript
// 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
```tsx
// 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
```typescript
// 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
```tsx
// 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
```tsx
// 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
```typescript
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
```tsx
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:
```tsx
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