Guides

React Integration

SPA-specific patterns for React, Next.js, and other frameworks

React Integration

This guide covers SPA-specific patterns for integrating BotSigged with React, Next.js, and similar frameworks. Single-page applications have unique considerations around component lifecycles, client-side routing, and form handling.

Installation

Install the SDK via npm or your preferred package manager:

npm install @botsigged/sdk
# or
yarn add @botsigged/sdk
# or
pnpm add @botsigged/sdk

Basic Setup

Provider Pattern

Create a context provider to manage the SDK instance across your application:

// lib/botsigged.tsx
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { BotSigged, ScoreUpdate } from '@botsigged/sdk';

interface BotSiggedContextValue {
  sdk: BotSigged | null;
  score: ScoreUpdate | null;
  isConnected: boolean;
  isReady: boolean;
}

const BotSiggedContext = createContext<BotSiggedContextValue>({
  sdk: null,
  score: null,
  isConnected: false,
  isReady: false,
});

export function BotSiggedProvider({
  children,
  apiKey
}: {
  children: ReactNode;
  apiKey: string;
}) {
  const [sdk, setSdk] = useState<BotSigged | null>(null);
  const [score, setScore] = useState<ScoreUpdate | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [isReady, setIsReady] = useState(false);

  useEffect(() => {
    const instance = new BotSigged({
      apiKey,
      autoStart: true,
      onScoreUpdate: (newScore) => {
        setScore(newScore);
        setIsReady(true);
      },
      onConnectionChange: setIsConnected,
    });

    setSdk(instance);

    return () => {
      instance.stop();
    };
  }, [apiKey]);

  return (
    <BotSiggedContext.Provider value={{ sdk, score, isConnected, isReady }}>
      {children}
    </BotSiggedContext.Provider>
  );
}

export function useBotSigged() {
  return useContext(BotSiggedContext);
}

Using the Provider

Wrap your application with the provider:

// app/layout.tsx (Next.js App Router)
import { BotSiggedProvider } from '@/lib/botsigged';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <BotSiggedProvider apiKey={process.env.NEXT_PUBLIC_BOTSIGGED_KEY!}>
          {children}
        </BotSiggedProvider>
      </body>
    </html>
  );
}
// pages/_app.tsx (Next.js Pages Router)
import { BotSiggedProvider } from '@/lib/botsigged';

export default function App({ Component, pageProps }) {
  return (
    <BotSiggedProvider apiKey={process.env.NEXT_PUBLIC_BOTSIGGED_KEY!}>
      <Component {...pageProps} />
    </BotSiggedProvider>
  );
}

Form Protection

Basic Protected Form

import { useBotSigged } from '@/lib/botsigged';
import { useState } from 'react';

export function SignupForm() {
  const { sdk, score, isReady } = useBotSigged();
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setError(null);
    setIsSubmitting(true);

    try {
      // Wait for bot detection before submitting
      const { score: finalScore, timedOut } = await sdk?.waitUntilReady() ?? {};

      if (finalScore && finalScore > 70) {
        setError('Unable to verify your request. Please try again.');
        return;
      }

      const formData = new FormData(e.currentTarget);
      await fetch('/api/signup', {
        method: 'POST',
        body: formData,
      });
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Sign Up'}
      </button>
      {error && <p className="error">{error}</p>}
    </form>
  );
}

Using withProtection

For cleaner code, use the withProtection wrapper:

import { useBotSigged } from '@/lib/botsigged';

export function ContactForm() {
  const { sdk } = useBotSigged();

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    // Wrap your submission logic
    const protectedSubmit = sdk?.withProtection(async () => {
      const response = await fetch('/api/contact', {
        method: 'POST',
        body: JSON.stringify(Object.fromEntries(formData)),
        headers: { 'Content-Type': 'application/json' },
      });
      return response.json();
    });

    await protectedSubmit?.();
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* form fields */}
    </form>
  );
}

Conditional Submit Button

Show users when verification is in progress:

import { useBotSigged } from '@/lib/botsigged';

export function SubmitButton() {
  const { sdk, isReady, score } = useBotSigged();

  const canSubmit = sdk?.canSubmit();
  const isBlocked = score && score.bot_score > 70;

  return (
    <button
      type="submit"
      disabled={!canSubmit?.allowed || isBlocked}
    >
      {!isReady ? 'Verifying...' : isBlocked ? 'Blocked' : 'Submit'}
    </button>
  );
}

Custom Hook

Create a custom hook for common patterns:

// hooks/useBotProtection.ts
import { useBotSigged } from '@/lib/botsigged';
import { useState, useCallback } from 'react';

interface UseProtectedSubmitOptions {
  threshold?: number;
  onBlocked?: (score: number) => void;
}

export function useProtectedSubmit<T>(
  submitFn: () => Promise<T>,
  options: UseProtectedSubmitOptions = {}
) {
  const { sdk } = useBotSigged();
  const { threshold = 70, onBlocked } = options;

  const [isSubmitting, setIsSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const submit = useCallback(async () => {
    setIsSubmitting(true);
    setError(null);

    try {
      const { score } = await sdk?.waitUntilReady() ?? {};

      if (score && score > threshold) {
        onBlocked?.(score);
        setError('Request blocked due to suspicious activity');
        return null;
      }

      return await submitFn();
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Submission failed');
      return null;
    } finally {
      setIsSubmitting(false);
    }
  }, [sdk, submitFn, threshold, onBlocked]);

  return { submit, isSubmitting, error };
}

Usage:

function MyForm() {
  const { submit, isSubmitting, error } = useProtectedSubmit(
    async () => {
      const res = await fetch('/api/submit', { method: 'POST' });
      return res.json();
    },
    { threshold: 60 }
  );

  return (
    <form onSubmit={(e) => { e.preventDefault(); submit(); }}>
      <button disabled={isSubmitting}>Submit</button>
      {error && <p>{error}</p>}
    </form>
  );
}

Score Display Component

For debugging or transparency, display the current score:

import { useBotSigged } from '@/lib/botsigged';

export function BotScoreIndicator() {
  const { score, isConnected, isReady } = useBotSigged();

  if (!isConnected) {
    return <span className="status disconnected">Disconnected</span>;
  }

  if (!isReady) {
    return <span className="status loading">Analyzing...</span>;
  }

  const classification = score?.classification ?? 'unknown';
  const botScore = score?.bot_score ?? 0;

  return (
    <div className={`score-indicator ${classification}`}>
      <span>Score: {botScore}</span>
      <span>{classification}</span>
    </div>
  );
}

Client-Side Routing

Handling Route Changes

The SDK automatically tracks page visibility, but for SPAs with client-side routing, you may want to track virtual page views:

// Next.js App Router
'use client';

import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
import { useBotSigged } from '@/lib/botsigged';

export function RouteTracker() {
  const pathname = usePathname();
  const { sdk } = useBotSigged();

  useEffect(() => {
    // SDK continues tracking across route changes
    // No action needed - behavioral signals persist
  }, [pathname, sdk]);

  return null;
}

Resetting on Logout

When a user logs out, you may want to end the current session:

async function handleLogout() {
  const { sdk } = useBotSigged();

  // Stop current session
  await sdk?.stop();

  // Clear auth state
  await signOut();

  // Optionally restart for the login page
  await sdk?.start();
}

Next.js Specific

App Router (Server Components)

The SDK must run on the client. Use the 'use client' directive:

// components/BotSiggedProvider.tsx
'use client';

import { BotSiggedProvider as Provider } from '@/lib/botsigged';

export function BotSiggedProvider({ children }: { children: React.ReactNode }) {
  return (
    <Provider apiKey={process.env.NEXT_PUBLIC_BOTSIGGED_KEY!}>
      {children}
    </Provider>
  );
}

Dynamic Import (Reduce Initial Bundle)

Load the SDK only when needed:

import dynamic from 'next/dynamic';

const BotSiggedProvider = dynamic(
  () => import('@/components/BotSiggedProvider').then(mod => mod.BotSiggedProvider),
  { ssr: false }
);

export default function Layout({ children }) {
  return <BotSiggedProvider>{children}</BotSiggedProvider>;
}

Middleware Integration

For server-side decisions, pass the session ID to your API routes:

const { sdk } = useBotSigged();

await fetch('/api/submit', {
  method: 'POST',
  headers: {
    'X-BotSigged-Session': sdk?.getSessionId() ?? '',
  },
  body: JSON.stringify(data),
});

Then verify server-side:

// app/api/submit/route.ts
export async function POST(request: Request) {
  const sessionId = request.headers.get('X-BotSigged-Session');

  // Query BotSigged API for session score
  const session = await fetch(`${BOTSIGGED_API}/sessions/${sessionId}`);
  const { bot_score } = await session.json();

  if (bot_score > 70) {
    return Response.json({ error: 'Blocked' }, { status: 403 });
  }

  // Process legitimate request
}

React Hook Form Integration

import { useForm } from 'react-hook-form';
import { useBotSigged } from '@/lib/botsigged';

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

export function ContactForm() {
  const { sdk } = useBotSigged();
  const { register, handleSubmit, formState } = useForm<FormData>();

  const onSubmit = async (data: FormData) => {
    const { score } = await sdk?.waitUntilReady() ?? {};

    if (score && score > 70) {
      throw new Error('Submission blocked');
    }

    await fetch('/api/contact', {
      method: 'POST',
      body: JSON.stringify({
        ...data,
        sessionId: sdk?.getSessionId(),
      }),
    });
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email', { required: true })} />
      <textarea {...register('message', { required: true })} />
      <button disabled={formState.isSubmitting}>Send</button>
    </form>
  );
}

Troubleshooting

SDK Not Initializing

Ensure the SDK is only initialized on the client:

useEffect(() => {
  // Only runs on client
  const instance = new BotSigged({ apiKey });
  // ...
}, []);

Score Always Null

The first score arrives after the WebSocket connects and initial analysis completes. Use isReady to track this:

const { score, isReady } = useBotSigged();

if (!isReady) {
  return <Loading />;
}

Multiple Instances

The SDK defaults to singleton mode with BotSigged.init(). In React with strict mode, use new BotSigged() with proper cleanup:

useEffect(() => {
  const instance = new BotSigged({ apiKey, autoStart: true });

  return () => {
    instance.stop(); // Clean up on unmount
  };
}, []);

Form Events Not Tracked

Ensure form elements are in the DOM when the SDK starts. For dynamically rendered forms, the SDK automatically observes new elements.

For forms in portals or iframes, you may need manual tracking (contact support for advanced use cases).