Skip to main content
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:
  1. Checking for window and document availability before accessing them
  2. Deferring script injection until the client-side hydration
  3. 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

The Turnstile component requires browser APIs and must be a client component.
Use useState and useEffect to ensure the component only renders on the client if you encounter issues.
Load the Turnstile script early for forms that are immediately visible.
Include Cloudflare’s domains in your Content Security Policy.

Build docs developers (and LLMs) love