@marsidev/react-turnstile
Version:
Cloudflare Turnstile integration for React.
409 lines (312 loc) • 9.33 kB
Markdown
---
name: token-lifecycle
description: >
Handle token generation, expiration, validation workflow, and form integration.
Activate when implementing form submission with CAPTCHA, handling token expiration,
or integrating with server-side validation.
triggers:
- 'turnstile token expired'
- 'turnstile getResponse'
- 'turnstile reset'
- 'turnstile form submit'
- 'turnstile onSuccess'
- 'turnstile onExpire'
- 'validate turnstile token'
- 'turnstile server validation'
category: lifecycle
metadata:
library: '@marsidev/react-turnstile'
library_version: '1.4.2'
framework: React
---
# Token Lifecycle
Handle token generation, expiration, validation workflow, and form integration.
## Understanding Tokens
Turnstile tokens are **single-use** and expire after a timeout (typically 5 minutes). Once validated by your server, they cannot be used again.
## Three Ways to Get Tokens
### 1. onSuccess Callback (Recommended)
Get the token when the user completes the challenge:
```tsx
import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'
export default function ContactForm() {
const [token, setToken] = useState<string | null>(null)
return (
<form>
<input type="email" placeholder="Email" />
<Turnstile
siteKey="YOUR_SITE_KEY"
onSuccess={(token) => setToken(token)}
/>
<button type="submit" disabled={!token}>
Submit
</button>
</form>
)
}
```
### 2. ref.current.getResponse()
Get the token imperatively at submission time:
```tsx
import { Turnstile } from '@marsidev/react-turnstile'
import type { TurnstileInstance } from '@marsidev/react-turnstile'
import { useRef } from 'react'
export default function ContactForm() {
const ref = useRef<TurnstileInstance>(null)
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
// Get token at the moment of submission
const token = ref.current?.getResponse()
if (!token) {
alert('Please complete the CAPTCHA')
return
}
await submitForm(token)
}
return (
<form onSubmit={handleSubmit}>
<input type="email" placeholder="Email" />
<Turnstile ref={ref} siteKey="YOUR_SITE_KEY" />
<button type="submit">Submit</button>
</form>
)
}
```
### 3. Hidden Form Field
The widget automatically adds a hidden input. Access it via FormData:
```tsx
export default function ContactForm() {
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const token = formData.get('cf-turnstile-response')
if (!token) {
alert('Please complete the CAPTCHA')
return
}
await submitForm(token)
}
return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" placeholder="Email" />
<Turnstile siteKey="YOUR_SITE_KEY" />
<button type="submit">Submit</button>
</form>
)
}
```
## Complete Form Integration
Best practice: Get token at submission time, validate server-side, reset widget:
```tsx
'use client'
import { Turnstile } from '@marsidev/react-turnstile'
import type { TurnstileInstance } from '@marsidev/react-turnstile'
import { useRef, useState } from 'react'
export default function ContactForm() {
const ref = useRef<TurnstileInstance>(null)
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setError(null)
const token = ref.current?.getResponse()
if (!token) {
setError('Please complete the CAPTCHA')
return
}
setIsSubmitting(true)
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, /* form data */ })
})
if (!response.ok) {
throw new Error('Submission failed')
}
// Success! Reset widget for potential re-submission
ref.current?.reset()
// Clear form, show success message, etc.
} catch (err) {
setError('Failed to submit. Please try again.')
// Reset widget on error too (token may be expired/used)
ref.current?.reset()
} finally {
setIsSubmitting(false)
}
}
return (
<form onSubmit={handleSubmit}>
{error && <div className="error">{error}</div>}
<input type="email" name="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<Turnstile
ref={ref}
siteKey="YOUR_SITE_KEY"
onExpire={() => {
// Optional: handle expiration
console.log('Token expired, please retry')
}}
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
)
}
```
## Handling Token Expiration
Tokens expire after ~5 minutes. Handle this with `onExpire` callback:
```tsx
<Turnstile
ref={ref}
siteKey="YOUR_SITE_KEY"
options={{ refreshExpired: 'manual' }}
onExpire={() => {
// Token expired - inform user or auto-reset
alert('Verification expired. Please try again.')
ref.current?.reset()
}}
/>
```
Or let it auto-refresh:
```tsx
<Turnstile
siteKey="YOUR_SITE_KEY"
options={{ refreshExpired: 'auto' }} // Default
/>
```
## Server-Side Validation
**Important:** The library provides TypeScript types but NO built-in validation. You must implement server-side validation yourself.
### Example API Route (Next.js)
```tsx
// app/api/verify/route.ts
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const { token } = await request.json()
const verification = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
secret: process.env.TURNSTILE_SECRET_KEY!,
response: token,
}),
}
)
const result = await verification.json()
if (!result.success) {
return NextResponse.json(
{ error: 'CAPTCHA validation failed' },
{ status: 400 }
)
}
return NextResponse.json({ success: true })
}
```
### Environment Variables
```bash
# .env.local (server-side only)
TURNSTILE_SECRET_KEY=0x0000000000000000000000000000000000000000000
```
**Never expose the secret key client-side!**
## Imperative API Methods
Control the widget programmatically:
### reset()
Reset the widget after submission or error:
```tsx
ref.current?.reset()
```
### getResponsePromise()
Wait for token with timeout (useful for programmatic flows):
```tsx
try {
const token = await ref.current?.getResponsePromise(30000) // 30s timeout
// Use token...
} catch (error) {
// Timeout or widget error
}
```
### isExpired()
Check if token has expired:
```tsx
if (ref.current?.isExpired()) {
ref.current?.reset()
}
```
### remove() and render()
Advanced: Fully remove and re-render the widget:
```tsx
ref.current?.remove() // Remove from DOM
ref.current?.render() // Re-render (only if previously removed)
```
## Common Mistakes
### ❌ Token Expires Before Form Submission
**Problem:** User takes too long to submit, token expires.
**Wrong:**
```tsx
const [token, setToken] = useState<string | null>(null)
<Turnstile onSuccess={setToken} />
// User delays submitting...
<button onClick={() => submit(token)}>Submit</button> // Token expired!
```
**Correct:**
```tsx
// Get token at submission time, not onSuccess
const token = ref.current?.getResponse()
await submit(token)
ref.current?.reset() // Reset for next time
```
### ❌ Expecting Built-in Server Validation
**Wrong:**
```tsx
import { validateTurnstile } from '@marsidev/react-turnstile'
// ❌ This doesn't exist!
```
**Correct:**
```tsx
// Implement your own server validation
const response = await fetch('/api/verify', {
method: 'POST',
body: JSON.stringify({ token })
})
```
### ❌ Calling Methods Before Widget Loads
**Wrong:**
```tsx
useEffect(() => {
// Widget not ready yet!
const token = ref.current?.getResponse() // undefined
}, [])
```
**Correct:**
```tsx
// Wait for onSuccess or user action
<Turnstile
ref={ref}
onSuccess={(token) => {
// Widget is ready
}}
/>
```
### ❌ Not Resetting After Validation
**Problem:** Token is single-use. After server validation, it's invalid.
**Correct:**
```tsx
const result = await validateToken(token)
if (result.success) {
// Process form...
ref.current?.reset() // Reset for potential re-submission
}
```
## Best Practices
1. **Get token at submission time** - Not onSuccess callback
2. **Always reset after use** - Tokens are single-use
3. **Handle expiration gracefully** - Inform users and provide retry
4. **Validate server-side** - Never trust client-side validation alone
5. **Keep secret key server-side** - Never expose in client code
## See Also
- [basic-setup skill](./basic-setup/SKILL.md) - Basic widget setup
- [multiple-widgets skill](./multiple-widgets/SKILL.md) - Managing tokens with multiple widgets
- [nextjs-ssr skill](./nextjs-ssr/SKILL.md) - Server Actions validation