Documentation Index
Fetch the complete documentation index at: https://mintlify.com/marsidev/react-turnstile/llms.txt
Use this file to discover all available pages before exploring further.
React Turnstile is designed to work seamlessly with server-side rendering (SSR) frameworks like Next.js, Remix, and others.
How SSR Works with Turnstile
The library handles SSR automatically by:
- Checking for
window and document availability before accessing them
- Deferring script injection until the client-side hydration
- Using React’s lifecycle methods to ensure proper timing
Next.js App Router
With Next.js 13+ App Router, you need to mark components using Turnstile as client components.
'use client'
import { Turnstile } from '@marsidev/react-turnstile'
export default function ContactForm() {
return (
<form>
<input name="email" type="email" />
<Turnstile siteKey="1x00000000000000000000AA" />
<button type="submit">Submit</button>
</form>
)
}
Why ‘use client’?
Turnstile requires browser APIs like window and document to:
- Inject the Cloudflare script dynamically
- Render the widget into the DOM
- Handle user interactions
The 'use client' directive tells Next.js to render this component on the client side.
Next.js Pages Router
With the Pages Router, no special configuration is needed:
import { Turnstile } from '@marsidev/react-turnstile'
export default function ContactPage() {
return (
<div>
<h1>Contact Us</h1>
<Turnstile siteKey="1x00000000000000000000AA" />
</div>
)
}
Manual Script Injection
For better control over script loading in SSR environments, you can inject the script manually:
'use client'
import Script from 'next/script'
import { Turnstile, SCRIPT_URL, DEFAULT_SCRIPT_ID } from '@marsidev/react-turnstile'
export default function Page() {
return (
<>
<Script
id={DEFAULT_SCRIPT_ID}
src={SCRIPT_URL}
strategy="beforeInteractive"
/>
<Turnstile
siteKey="1x00000000000000000000AA"
injectScript={false}
/>
</>
)
}
Script Loading Strategies
Next.js provides several strategies for loading external scripts:
beforeInteractive: Load before page becomes interactive (recommended for Turnstile)
afterInteractive: Load after page becomes interactive
lazyOnload: Load during idle time
Remix
In Remix, components are server-rendered by default. Mark your Turnstile component as client-only:
import { Turnstile } from '@marsidev/react-turnstile'
export default function ContactRoute() {
return (
<div>
<h1>Contact</h1>
{typeof window !== 'undefined' && (
<Turnstile siteKey="1x00000000000000000000AA" />
)}
</div>
)
}
Or use Remix’s ClientOnly component:
import { ClientOnly } from 'remix-utils/client-only'
import { Turnstile } from '@marsidev/react-turnstile'
export default function ContactRoute() {
return (
<div>
<h1>Contact</h1>
<ClientOnly>
{() => <Turnstile siteKey="1x00000000000000000000AA" />}
</ClientOnly>
</div>
)
}
Handling Hydration Mismatches
If you encounter hydration warnings, ensure the component is only rendered on the client:
import { useEffect, useState } from 'react'
import { Turnstile } from '@marsidev/react-turnstile'
export default function TurnstileWrapper() {
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
if (!isClient) {
return <div style={{ width: 300, height: 65 }} /> // Placeholder
}
return <Turnstile siteKey="1x00000000000000000000AA" />
}
Script Loading in SSR
The library automatically handles script injection, but you can monitor the loading state:
'use client'
import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'
export default function Form() {
const [scriptLoaded, setScriptLoaded] = useState(false)
return (
<div>
{!scriptLoaded && <p>Loading security check...</p>}
<Turnstile
siteKey="1x00000000000000000000AA"
onLoadScript={() => setScriptLoaded(true)}
/>
</div>
)
}
Content Security Policy (CSP)
When using SSR with CSP headers, ensure you allow Cloudflare’s domains:
Content-Security-Policy:
script-src 'self' https://challenges.cloudflare.com;
frame-src 'self' https://challenges.cloudflare.com;
For Next.js, add this to your next.config.js:
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: "script-src 'self' https://challenges.cloudflare.com; frame-src 'self' https://challenges.cloudflare.com;"
}
]
}
]
}
}
Server-Side Validation
After receiving the token on the client, validate it server-side:
// app/api/verify/route.ts (Next.js App Router)
import type { TurnstileServerValidationResponse } from '@marsidev/react-turnstile'
export async function POST(request: Request) {
const { token } = await request.json()
const response = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body: `secret=${encodeURIComponent(process.env.TURNSTILE_SECRET_KEY)}&response=${encodeURIComponent(token)}`,
}
)
const data: TurnstileServerValidationResponse = await response.json()
return Response.json({ success: data.success })
}
Best Practices
Always use 'use client' in Next.js App Router
The Turnstile component requires browser APIs and must be a client component.
Prevent hydration mismatches
Use useState and useEffect to ensure the component only renders on the client if you encounter issues.
Use beforeInteractive for critical forms
Load the Turnstile script early for forms that are immediately visible.
Configure CSP headers properly