Skip to main content

Overview

reCAPTCHA tokens are valid for approximately 2 minutes. For forms that may be open for longer periods or for repeated submissions, you’ll need to refresh the token. This guide shows you different approaches to handle token refresh scenarios.

Why Refresh Tokens?

You should refresh reCAPTCHA tokens in these scenarios:
  • Long forms: When users might take more than 2 minutes to complete a form
  • Repeated submissions: When users can submit the same form multiple times
  • Failed submissions: When a submission fails and the user needs to retry
  • Session persistence: When keeping a form open for extended periods
reCAPTCHA tokens expire after approximately 2 minutes. Using an expired token will result in verification failure on your backend.

Using the GoogleReCaptcha Component

The GoogleReCaptcha component provides a built-in way to refresh tokens using the refreshReCaptcha prop:
FormWithAutoRefresh.jsx
import React, { useState, useCallback } from 'react';
import {
  GoogleReCaptchaProvider,
  GoogleReCaptcha
} from 'react-google-recaptcha-v3';

export function FormWithAutoRefresh() {
  const [token, setToken] = useState('');
  const [refreshReCaptcha, setRefreshReCaptcha] = useState(false);
  const [isSubmitting, setIsSubmitting] = useState(false);

  // Use useCallback to prevent infinite re-renders
  const onVerify = useCallback((token) => {
    setToken(token);
    console.log('New token received:', token);
  }, []);

  const handleSubmit = async (event) => {
    event.preventDefault();
    setIsSubmitting(true);

    try {
      // Submit your form with the current token
      const response = await fetch('/api/submit', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token }),
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.message);
      }

      alert('Form submitted successfully!');
      
      // Refresh reCAPTCHA after successful submission
      setRefreshReCaptcha(r => !r);
    } catch (error) {
      alert('Submission failed: ' + error.message);
      
      // Refresh reCAPTCHA after failed submission
      setRefreshReCaptcha(r => !r);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <GoogleReCaptchaProvider reCaptchaKey="YOUR_RECAPTCHA_SITE_KEY">
      <form onSubmit={handleSubmit}>
        {/* Hidden component that manages reCAPTCHA */}
        <GoogleReCaptcha
          onVerify={onVerify}
          refreshReCaptcha={refreshReCaptcha}
        />
        
        <div className="form-group">
          <label htmlFor="message">Your Message</label>
          <textarea id="message" name="message" rows="5" required />
        </div>

        <button type="submit" disabled={isSubmitting || !token}>
          {isSubmitting ? 'Submitting...' : 'Submit'}
        </button>

        {token && (
          <p className="token-info">
            Token ready: {token.substring(0, 20)}...
          </p>
        )}
      </form>
    </GoogleReCaptchaProvider>
  );
}
The refreshReCaptcha prop accepts any value. When this value changes, the component will request a new token. Common patterns include toggling a boolean or using a timestamp.

Manual Token Refresh with useGoogleReCaptcha

For more control, you can manually request new tokens using the useGoogleReCaptcha hook:
ManualRefreshForm.jsx
import React, { useState, useCallback, useEffect } from 'react';
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';

export function ManualRefreshForm() {
  const { executeRecaptcha } = useGoogleReCaptcha();
  const [token, setToken] = useState('');
  const [tokenAge, setTokenAge] = useState(0);
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: '',
  });

  // Function to get a fresh token
  const refreshToken = useCallback(async () => {
    if (!executeRecaptcha) {
      console.log('Execute recaptcha not available yet');
      return;
    }

    try {
      const newToken = await executeRecaptcha('submit_form');
      setToken(newToken);
      setTokenAge(0);
      console.log('Token refreshed');
    } catch (error) {
      console.error('Failed to refresh token:', error);
    }
  }, [executeRecaptcha]);

  // Get initial token when component mounts
  useEffect(() => {
    refreshToken();
  }, [refreshToken]);

  // Track token age and auto-refresh after 90 seconds
  useEffect(() => {
    if (!token) return;

    const interval = setInterval(() => {
      setTokenAge(age => {
        const newAge = age + 1;
        
        // Auto-refresh after 90 seconds (before 2-minute expiration)
        if (newAge >= 90) {
          refreshToken();
          return 0;
        }
        
        return newAge;
      });
    }, 1000);

    return () => clearInterval(interval);
  }, [token, refreshToken]);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  };

  const handleSubmit = async (event) => {
    event.preventDefault();

    // Get a fresh token right before submission
    await refreshToken();

    try {
      const response = await fetch('/api/submit', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          ...formData,
          recaptchaToken: token,
        }),
      });

      if (!response.ok) {
        throw new Error('Submission failed');
      }

      alert('Form submitted successfully!');
      setFormData({ name: '', email: '', message: '' });
      
      // Get a new token for potential re-submission
      await refreshToken();
    } catch (error) {
      alert('Error: ' + error.message);
      // Refresh token after error
      await refreshToken();
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div className="token-status">
        Token age: {tokenAge}s
        {tokenAge > 60 && <span className="warning"> (will refresh soon)</span>}
        <button type="button" onClick={refreshToken}>
          Refresh Now
        </button>
      </div>

      <div className="form-group">
        <label htmlFor="name">Name</label>
        <input
          id="name"
          name="name"
          type="text"
          value={formData.name}
          onChange={handleChange}
          required
        />
      </div>

      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input
          id="email"
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
          required
        />
      </div>

      <div className="form-group">
        <label htmlFor="message">Message</label>
        <textarea
          id="message"
          name="message"
          value={formData.message}
          onChange={handleChange}
          required
          rows="5"
        />
      </div>

      <button type="submit" disabled={!token}>
        Submit
      </button>
    </form>
  );
}
Auto-refresh tokens after 90 seconds to ensure you always have a valid token before the 2-minute expiration.

Refresh on Submission Failure

Always refresh the token after a failed submission:
FailureHandling.jsx
import React, { useState, useCallback } from 'react';
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';

export function FailureHandling() {
  const { executeRecaptcha } = useGoogleReCaptcha();
  const [email, setEmail] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [error, setError] = useState('');
  const [retryCount, setRetryCount] = useState(0);

  const handleSubmit = useCallback(async (event) => {
    event.preventDefault();
    setIsSubmitting(true);
    setError('');

    try {
      if (!executeRecaptcha) {
        throw new Error('reCAPTCHA not ready');
      }

      // Always get a fresh token for each submission attempt
      const token = await executeRecaptcha('newsletter_signup');

      const response = await fetch('/api/newsletter/subscribe', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, recaptchaToken: token }),
      });

      const data = await response.json();

      if (!response.ok) {
        if (response.status === 429) {
          throw new Error('Too many attempts. Please try again later.');
        }
        throw new Error(data.message || 'Subscription failed');
      }

      // Success!
      alert('Successfully subscribed to newsletter!');
      setEmail('');
      setRetryCount(0);
    } catch (err) {
      setError(err.message);
      setRetryCount(prev => prev + 1);
      
      // Note: A new token will be obtained on next submission
      console.log(`Submission failed. Retry count: ${retryCount + 1}`);
    } finally {
      setIsSubmitting(false);
    }
  }, [email, executeRecaptcha, retryCount]);

  return (
    <form onSubmit={handleSubmit}>
      <h2>Subscribe to Newsletter</h2>
      
      {error && (
        <div className="error-message">
          {error}
          {retryCount > 0 && (
            <small> (Attempt {retryCount + 1})</small>
          )}
        </div>
      )}

      <div className="form-group">
        <label htmlFor="email">Email Address</label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
          disabled={isSubmitting}
        />
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Subscribing...' : 'Subscribe'}
      </button>
    </form>
  );
}

Multi-Step Form with Token Refresh

For multi-step forms, refresh the token at the final submission:
MultiStepForm.jsx
import React, { useState, useCallback } from 'react';
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';

export function MultiStepForm() {
  const { executeRecaptcha } = useGoogleReCaptcha();
  const [step, setStep] = useState(1);
  const [formData, setFormData] = useState({
    // Step 1
    name: '',
    email: '',
    // Step 2
    address: '',
    city: '',
    // Step 3
    cardNumber: '',
    expiryDate: '',
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  };

  const nextStep = () => setStep(prev => prev + 1);
  const prevStep = () => setStep(prev => prev - 1);

  const handleFinalSubmit = useCallback(async () => {
    if (!executeRecaptcha) {
      alert('reCAPTCHA not ready. Please try again.');
      return;
    }

    try {
      // Get a fresh token right before final submission
      const token = await executeRecaptcha('checkout');

      const response = await fetch('/api/checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          ...formData,
          recaptchaToken: token,
        }),
      });

      if (!response.ok) {
        throw new Error('Checkout failed');
      }

      alert('Order completed successfully!');
      // Redirect to success page
      window.location.href = '/success';
    } catch (error) {
      alert('Error: ' + error.message);
    }
  }, [formData, executeRecaptcha]);

  return (
    <div className="multi-step-form">
      <div className="progress">
        Step {step} of 3
      </div>

      {step === 1 && (
        <div className="step">
          <h2>Personal Information</h2>
          <input
            name="name"
            placeholder="Full Name"
            value={formData.name}
            onChange={handleChange}
          />
          <input
            name="email"
            type="email"
            placeholder="Email"
            value={formData.email}
            onChange={handleChange}
          />
          <button onClick={nextStep}>Next</button>
        </div>
      )}

      {step === 2 && (
        <div className="step">
          <h2>Shipping Address</h2>
          <input
            name="address"
            placeholder="Street Address"
            value={formData.address}
            onChange={handleChange}
          />
          <input
            name="city"
            placeholder="City"
            value={formData.city}
            onChange={handleChange}
          />
          <button onClick={prevStep}>Back</button>
          <button onClick={nextStep}>Next</button>
        </div>
      )}

      {step === 3 && (
        <div className="step">
          <h2>Payment Information</h2>
          <input
            name="cardNumber"
            placeholder="Card Number"
            value={formData.cardNumber}
            onChange={handleChange}
          />
          <input
            name="expiryDate"
            placeholder="MM/YY"
            value={formData.expiryDate}
            onChange={handleChange}
          />
          <button onClick={prevStep}>Back</button>
          <button onClick={handleFinalSubmit}>Complete Order</button>
          <p className="note">
            Your information is protected by reCAPTCHA
          </p>
        </div>
      )}
    </div>
  );
}
For multi-step forms, you typically only need to get a reCAPTCHA token at the final submission step, not for each intermediate step.

Polling Forms (Repeated Submissions)

For forms that allow repeated submissions, manage token lifecycle carefully:
PollingForm.jsx
import React, { useState, useCallback, useEffect, useRef } from 'react';
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';

export function PollingForm() {
  const { executeRecaptcha } = useGoogleReCaptcha();
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [lastToken, setLastToken] = useState('');
  const [lastTokenTime, setLastTokenTime] = useState(0);
  const tokenTimeoutRef = useRef(null);

  // Get a fresh token
  const getToken = useCallback(async () => {
    if (!executeRecaptcha) {
      return null;
    }

    const now = Date.now();
    
    // Reuse token if it's less than 90 seconds old
    if (lastToken && (now - lastTokenTime) < 90000) {
      console.log('Reusing existing token');
      return lastToken;
    }

    console.log('Getting fresh token');
    const token = await executeRecaptcha('search_query');
    setLastToken(token);
    setLastTokenTime(now);

    // Clear any existing timeout
    if (tokenTimeoutRef.current) {
      clearTimeout(tokenTimeoutRef.current);
    }

    // Schedule token refresh after 90 seconds
    tokenTimeoutRef.current = setTimeout(() => {
      setLastToken('');
    }, 90000);

    return token;
  }, [executeRecaptcha, lastToken, lastTokenTime]);

  const handleSearch = useCallback(async () => {
    if (!query.trim()) return;

    try {
      const token = await getToken();
      
      if (!token) {
        alert('reCAPTCHA not ready');
        return;
      }

      const response = await fetch('/api/search', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          query,
          recaptchaToken: token,
        }),
      });

      const data = await response.json();
      setResults(data.results);
    } catch (error) {
      console.error('Search failed:', error);
    }
  }, [query, getToken]);

  // Cleanup timeout on unmount
  useEffect(() => {
    return () => {
      if (tokenTimeoutRef.current) {
        clearTimeout(tokenTimeoutRef.current);
      }
    };
  }, []);

  return (
    <div>
      <div className="search-box">
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Search..."
          onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
        />
        <button onClick={handleSearch}>Search</button>
      </div>

      <div className="results">
        {results.map((result, index) => (
          <div key={index} className="result-item">
            {result.title}
          </div>
        ))}
      </div>
    </div>
  );
}

Best Practices

  1. Refresh before expiration: Request a new token after 90 seconds to stay ahead of the 2-minute expiration
  2. Refresh on failure: Always get a new token after a failed submission
  3. One token per submission: Don’t reuse tokens across multiple form submissions
  4. User-initiated refresh: Provide a manual refresh option for users who keep forms open for a long time
  5. Handle edge cases: Account for slow network connections and loading states
Never store tokens in localStorage or cookies. Always request fresh tokens as needed.

Next Steps

Build docs developers (and LLMs) love