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
submitevent in the capture phase to prevent native submission - Use
FormDataor 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
useEffectreturn 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
- Use
useReffor form references: Don't rely on state for form elements, use refs to access the DOM directly - Clean up event listeners: Always remove event listeners in the
useEffectcleanup function - Handle loading states: Show loading indicators while SpamBlock is checking
- Reset form on success: Clear the form after successful submission
- TypeScript types: Define proper types for SpamBlock events and responses
- Error handling: Always provide user-friendly error messages based on SpamBlock's
reasonsarray
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 |