React & Next.js Integration

Integrate SpamBlock with React and Next.js forms using hooks and event listeners

Overview

This guide shows how to integrate SpamBlock with React and Next.js applications. You'll learn how to use React hooks to handle SpamBlock events, manage form state, and display success/error messages with modern React patterns.

Prerequisites

  • React 16.8+ (for hooks) or Next.js 12+
  • Basic knowledge of React hooks (useEffect, useRef, useState)
  • SpamBlock pixel script included

Setup

1. Include SpamBlock Pixel

Next.js (App Router)

Add the script to your app/layout.tsx or pages/_document.tsx:

// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <Script
          src="https://api.spamblock.io/sdk/pixel/v1.js"
          strategy="afterInteractive"
          defer
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

Next.js (Pages Router)

// pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document';
import Script from 'next/script';

export default function Document() {
  return (
    <Html>
      <Head>
        <Script
          src="https://api.spamblock.io/sdk/pixel/v1.js"
          strategy="afterInteractive"
          defer
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

Create React App / Vite

Add to your public/index.html:

<script src="https://api.spamblock.io/sdk/pixel/v1.js" defer></script>

2. Basic React Form Component

Here's a contact form component with SpamBlock integration:

import { useEffect, useRef, useState } from 'react';

interface FormStatus {
  type: 'idle' | 'loading' | 'success' | 'error';
  message: string;
}

export function ContactForm() {
  const formRef = useRef<HTMLFormElement>(null);
  const [status, setStatus] = useState<FormStatus>({ type: 'idle', message: '' });
  const [isSubmitting, setIsSubmitting] = useState(false);

  useEffect(() => {
    const form = formRef.current;
    if (!form) return;

    // Mark form for SpamBlock protection
    form.setAttribute('data-block-spam', 'true');

    // Handle successful submission
    const handleAllowed = (event: Event) => {
      const customEvent = event as CustomEvent;
      const response = customEvent.detail?.response;
      
      setIsSubmitting(false);
      
      if (response && response.allow === true) {
        setStatus({
          type: 'success',
          message: '✅ Thank you! Your message has been sent successfully.'
        });
        form.reset();
        
        // Clear success message after 5 seconds
        setTimeout(() => {
          setStatus({ type: 'idle', message: '' });
        }, 5000);
      }
    };

    // Handle blocked submission
    const handleBlocked = (event: Event) => {
      const customEvent = event as CustomEvent;
      const response = customEvent.detail?.response;
      const reasons = response?.reasons || [];
      
      setIsSubmitting(false);
      
      let errorMsg = '❌ Your submission was blocked. ';
      
      if (reasons.includes('disposable_domain')) {
        errorMsg += 'Please use a valid email address.';
      } else if (reasons.includes('profanity_detected')) {
        errorMsg += 'Please remove inappropriate language.';
      } else if (reasons.includes('honeypot_filled')) {
        errorMsg += 'Automated submission detected.';
      } else {
        errorMsg += `Spam detected (score: ${response?.score || 0}).`;
      }
      
      setStatus({ type: 'error', message: errorMsg });
    };

    // Show loading state on submit
    const handleSubmit = () => {
      setStatus({ type: 'idle', message: '' });
      setIsSubmitting(true);
    };

    form.addEventListener('spamblock:allowed', handleAllowed);
    form.addEventListener('spamblock:blocked', handleBlocked);
    form.addEventListener('submit', handleSubmit);

    return () => {
      form.removeEventListener('spamblock:allowed', handleAllowed);
      form.removeEventListener('spamblock:blocked', handleBlocked);
      form.removeEventListener('submit', handleSubmit);
    };
  }, []);

  return (
    <form
      ref={formRef}
      action="/api/contact"
      method="post"
      className="max-w-2xl mx-auto space-y-6"
    >
      <div>
        <label htmlFor="email" className="block text-sm font-medium text-slate-700 mb-2">
          Email Address
        </label>
        <input
          type="email"
          id="email"
          name="email"
          required
          placeholder="[email protected]"
          className="w-full px-4 py-2 border border-slate-300 rounded-lg shadow-sm 
                     focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
        />
      </div>

      <div>
        <label htmlFor="message" className="block text-sm font-medium text-slate-700 mb-2">
          Message
        </label>
        <textarea
          id="message"
          name="message"
          rows={5}
          required
          placeholder="Your message here..."
          className="w-full px-4 py-2 border border-slate-300 rounded-lg shadow-sm 
                     focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500
                     resize-none"
        />
      </div>

      <button
        type="submit"
        disabled={isSubmitting}
        className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg 
                   hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 
                   focus:ring-offset-2 font-medium transition-colors disabled:opacity-50 
                   disabled:cursor-not-allowed flex items-center justify-center gap-2"
      >
        {isSubmitting && (
          <svg className="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
            <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
            <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
          </svg>
        )}
        <span>{isSubmitting ? 'Sending...' : 'Send Message'}</span>
      </button>

      {/* Status messages */}
      {status.type === 'success' && (
        <div className="rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
          {status.message}
        </div>
      )}

      {status.type === 'error' && (
        <div className="rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700">
          {status.message}
        </div>
      )}
    </form>
  );
}

Submission Methods

React applications typically use JavaScript-based submission rather than native form submission. This is because:

  • SPAs need to stay on the same page
  • React apps typically use API endpoints, not form actions
  • You need full control over the submission flow and UI updates

Native Form Submission (Rare in React)

If you're using React but want native form submission (form navigates to action URL), you can simply let SpamBlock handle it:

export function ContactForm() {
  const formRef = useRef<HTMLFormElement>(null);

  useEffect(() => {
    if (formRef.current) {
      formRef.current.setAttribute('data-block-spam', 'true');
    }
  }, []);

  // No event handlers needed - SpamBlock will submit natively
  return (
    <form ref={formRef} action="/api/contact" method="post">
      <input type="email" name="email" required />
      <textarea name="message" required></textarea>
      <button type="submit">Send</button>
    </form>
  );
}

JavaScript-Based Submission (Recommended for React)

For most React apps, you'll want to intercept the spamblock:allowed event and handle submission manually:

useEffect(() => {
  const form = formRef.current;
  if (!form) return;

  form.setAttribute('data-block-spam', 'true');

  // Prevent native submission by intercepting submit event in capture phase
  const handleSubmit = (event: Event) => {
    // This runs before SpamBlock's handler, preventing native submission
    event.preventDefault();
  };

  const handleAllowed = async (event: Event) => {
    const customEvent = event as CustomEvent;
    const response = customEvent.detail?.response;
    
    if (response && response.allow === true) {
      // Submit via fetch or your API client
      const formData = new FormData(form);
      const result = await fetch('/api/contact', {
        method: 'POST',
        body: formData
      });
      
      if (result.ok) {
        setStatus({ type: 'success', message: '✅ Message sent!' });
        form.reset();
      }
    }
  };

  form.addEventListener('submit', handleSubmit, { capture: true });
  form.addEventListener('spamblock:allowed', handleAllowed);
  return () => {
    form.removeEventListener('submit', handleSubmit, { capture: true });
    form.removeEventListener('spamblock:allowed', handleAllowed);
  };
}, []);

Key points:

  • Intercept the submit event in the capture phase to prevent native submission
  • Use FormData or your form library's data collection method
  • Handle submission with your API client (fetch, axios, etc.)
  • Update React state to show success/error messages
  • Don't forget to clean up both event listeners in the useEffect return function

Advanced Examples

Custom Hook for SpamBlock

Create a reusable hook for SpamBlock integration:

// hooks/useSpamBlock.ts
import { useEffect, useRef, useState } from 'react';

interface SpamBlockResponse {
  allow: boolean;
  score: number;
  reasons: string[];
  latencyMs: number;
}

interface UseSpamBlockReturn {
  isSubmitting: boolean;
  status: 'idle' | 'success' | 'error';
  message: string;
  response: SpamBlockResponse | null;
}

export function useSpamBlock(formRef: React.RefObject<HTMLFormElement>): UseSpamBlockReturn {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle');
  const [message, setMessage] = useState('');
  const [response, setResponse] = useState<SpamBlockResponse | null>(null);

  useEffect(() => {
    const form = formRef.current;
    if (!form) return;

    form.setAttribute('data-block-spam', 'true');

    const handleAllowed = (event: Event) => {
      const customEvent = event as CustomEvent;
      const res = customEvent.detail?.response;
      
      setIsSubmitting(false);
      setResponse(res);
      
      if (res && res.allow === true) {
        setStatus('success');
        setMessage('✅ Thank you! Your message has been sent successfully.');
        form.reset();
        
        setTimeout(() => {
          setStatus('idle');
          setMessage('');
        }, 5000);
      }
    };

    const handleBlocked = (event: Event) => {
      const customEvent = event as CustomEvent;
      const res = customEvent.detail?.response;
      const reasons = res?.reasons || [];
      
      setIsSubmitting(false);
      setResponse(res);
      setStatus('error');
      
      let errorMsg = '❌ Your submission was blocked. ';
      if (reasons.includes('disposable_domain')) {
        errorMsg += 'Please use a valid email address.';
      } else if (reasons.includes('profanity_detected')) {
        errorMsg += 'Please remove inappropriate language.';
      } else {
        errorMsg += `Spam detected (score: ${res?.score || 0}).`;
      }
      
      setMessage(errorMsg);
    };

    const handleSubmit = () => {
      setStatus('idle');
      setMessage('');
      setIsSubmitting(true);
    };

    form.addEventListener('spamblock:allowed', handleAllowed);
    form.addEventListener('spamblock:blocked', handleBlocked);
    form.addEventListener('submit', handleSubmit);

    return () => {
      form.removeEventListener('spamblock:allowed', handleAllowed);
      form.removeEventListener('spamblock:blocked', handleBlocked);
      form.removeEventListener('submit', handleSubmit);
    };
  }, [formRef]);

  return { isSubmitting, status, message, response };
}

// Usage in component
export function ContactForm() {
  const formRef = useRef<HTMLFormElement>(null);
  const { isSubmitting, status, message } = useSpamBlock(formRef);

  return (
    <form ref={formRef} action="/api/contact" method="post">
      {/* Form fields */}
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Sending...' : 'Send Message'}
      </button>
      {status === 'success' && <div className="success">{message}</div>}
      {status === 'error' && <div className="error">{message}</div>}
    </form>
  );
}

Next.js Server Actions Integration

For Next.js App Router with Server Actions:

// app/contact/page.tsx
'use client';

import { useRef, useState } from 'react';
import { submitContactForm } from './actions';

export function ContactForm() {
  const formRef = useRef<HTMLFormElement>(null);
  const [status, setStatus] = useState<{ type: 'idle' | 'success' | 'error'; message: string }>({
    type: 'idle',
    message: ''
  });

  useEffect(() => {
    const form = formRef.current;
    if (!form) return;

    form.setAttribute('data-block-spam', 'true');

    const handleAllowed = async (event: Event) => {
      const customEvent = event as CustomEvent;
      const response = customEvent.detail?.response;
      
      if (response && response.allow === true) {
        // Form will submit normally via Server Action
        // You can track the success here
        setStatus({
          type: 'success',
          message: '✅ Thank you! Your message has been sent successfully.'
        });
      }
    };

    const handleBlocked = (event: Event) => {
      const customEvent = event as CustomEvent;
      const response = customEvent.detail?.response;
      
      setStatus({
        type: 'error',
        message: `❌ Submission blocked: ${response?.reasons?.join(', ') || 'Spam detected'}`
      });
    };

    form.addEventListener('spamblock:allowed', handleAllowed);
    form.addEventListener('spamblock:blocked', handleBlocked);

    return () => {
      form.removeEventListener('spamblock:allowed', handleAllowed);
      form.removeEventListener('spamblock:blocked', handleBlocked);
    };
  }, []);

  return (
    <form ref={formRef} action={submitContactForm}>
      {/* Form fields */}
    </form>
  );
}

React Hook Form Integration

Integrate with React Hook Form:

import { useForm } from 'react-hook-form';
import { useEffect, useRef } from 'react';

interface FormData {
  email: string;
  message: string;
}

export function ContactForm() {
  const formRef = useRef<HTMLFormElement>(null);
  const { register, handleSubmit, reset, formState: { isSubmitting } } = useForm<FormData>();
  const [status, setStatus] = useState<{ type: 'success' | 'error'; message: string } | null>(null);

  useEffect(() => {
    const form = formRef.current;
    if (!form) return;

    form.setAttribute('data-block-spam', 'true');

    const handleAllowed = () => {
      setStatus({
        type: 'success',
        message: '✅ Thank you! Your message has been sent successfully.'
      });
      reset();
    };

    const handleBlocked = (event: Event) => {
      const customEvent = event as CustomEvent;
      const response = customEvent.detail?.response;
      
      setStatus({
        type: 'error',
        message: `❌ Submission blocked: ${response?.reasons?.join(', ') || 'Spam detected'}`
      });
    };

    form.addEventListener('spamblock:allowed', handleAllowed);
    form.addEventListener('spamblock:blocked', handleBlocked);

    return () => {
      form.removeEventListener('spamblock:allowed', handleAllowed);
      form.removeEventListener('spamblock:blocked', handleBlocked);
    };
  }, [reset]);

  const onSubmit = async (data: FormData) => {
    // This will be called after SpamBlock allows the submission
    await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email', { required: true })} type="email" />
      <textarea {...register('message', { required: true })} />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Sending...' : 'Send Message'}
      </button>
      {status && (
        <div className={status.type === 'success' ? 'success' : 'error'}>
          {status.message}
        </div>
      )}
    </form>
  );
}

Best Practices

  1. Use useRef for form references: Don't rely on state for form elements, use refs to access the DOM directly
  2. Clean up event listeners: Always remove event listeners in the useEffect cleanup function
  3. Handle loading states: Show loading indicators while SpamBlock is checking
  4. Reset form on success: Clear the form after successful submission
  5. TypeScript types: Define proper types for SpamBlock events and responses
  6. Error handling: Always provide user-friendly error messages based on SpamBlock's reasons array

Troubleshooting

Issue Solution
Events not firing Ensure data-block-spam is set in useEffect after component mounts
Form submits twice Check that you're not manually calling form.submit() in addition to SpamBlock
TypeScript errors Install @types/react and ensure proper typing for CustomEvent
Next.js hydration errors Use useEffect to set data-block-spam attribute, not during render
Events in SSR SpamBlock events only work client-side; ensure script loads after hydration