@clickstreamhq/react

Official React adapter. It configures @clickstreamhq/signals in React and gives components a stable hook surface for visitor context.

Tracking still comes from the browser pixel. Install the pixel with the script tag or @clickstreamhq/sdk so _cs_uid, _cs_sid, window.clickstream.identify(), and window.clickstream.trackEvent() exist before your hooks need them.

Peer deps: react ^18 || ^19. Runtime deps: @clickstreamhq/signals.

Use the React adapter when a component needs to react to a ClickStream lane:

Install

pnpm add @clickstreamhq/react @clickstreamhq/sdk @clickstreamhq/signals

Provider setup

Wrap your app — usually in the top-level client component or the Next.js App Router root layout:

// app/providers.tsx
'use client';

import { useEffect } from 'react';
import { installClickstreamPixel } from '@clickstreamhq/sdk';
import { ClickStreamProvider } from '@clickstreamhq/react';

export function Providers({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    installClickstreamPixel({
      apiKey: process.env.NEXT_PUBLIC_CLICKSTREAM_KEY!,
      endpoint: 'https://t.example.com',
      replay: true,
    });
  }, []);

  return (
    <ClickStreamProvider
      apiKey={process.env.NEXT_PUBLIC_CLICKSTREAM_KEY!}
      endpoint="https://t.example.com"
    >
      {children}
    </ClickStreamProvider>
  );
}

Replace t.example.com with your first-party tracking domain. The endpoint is used by both the SDK and the signals client.

If your app already has the static script tag in <head>, you can omit installClickstreamPixel() and keep the provider.

Copy-Paste: Lane-Aware Component

'use client';

import { useVisitor } from '@clickstreamhq/react';

export function PageAction() {
  const { ctx: visitor, loading } = useVisitor();

  if (loading || !visitor) return <DefaultAction />;

  if (!visitor.bot.isBot && visitor.behavioralClass === 'human') {
    if (visitor.scores.intent >= 70) return <HumanDetailView />;
    if (visitor.scores.frustration >= 60) return <HelpPanel />;
    return <DefaultAction />;
  }

  if (
    visitor.bot.category === 'ai_agent' ||
    visitor.bot.category === 'search_crawler'
  ) {
    return <MachineReadableAction />;
  }

  if (visitor.bot.category === 'automation') {
    return <QaCoverageMarker sessionId={visitor.session.sessionId} />;
  }

  return <AccessibleStaticAction />;
}

The important rule is simple: human-only UI checks for behavioralClass === 'human' and bot.isBot === false. Non-human lanes stay measurable, but they do not trigger human-only actions.

Hooks

useVisitor(): { ctx, loading, error }

Returns the current VisitorContext plus loading/error state. ctx is null while the signals endpoint warms up or when no cookie is set.

'use client';

import { useVisitor } from '@clickstreamhq/react';

export function DocumentationPanel() {
  const { ctx: visitor, loading } = useVisitor();

  if (loading || !visitor) return <DefaultDocs />;
  if (visitor.scores.intent >= 70) return <DetailedView />;
  if (visitor.scores.frustration >= 60) return <HelpView />;
  return <DefaultView />;
}

The hook re-renders on every VisitorContext update — internally a 2-second poll of the signals endpoint. Values below 1000 ms are clamped to 1000 ms.

Human-only UI recipe

'use client';

import { useVisitor, useTrack } from '@clickstreamhq/react';

export function PriorityActionButton() {
  const { ctx } = useVisitor();
  const track = useTrack();

  const shouldShow =
    ctx &&
    !ctx.bot.isBot &&
    ctx.behavioralClass === 'human' &&
    ctx.scores.conversionReadiness >= 70;

  if (!shouldShow) return null;

  return (
    <button onClick={() => track({ name: 'priority_action_clicked', category: 'engagement' })}>
      Continue
    </button>
  );
}

AI/search structured-content recipe

'use client';

import { useVisitor } from '@clickstreamhq/react';

export function StructuredFaq({ faqs }) {
  const { ctx } = useVisitor();
  const discoveryLane =
    ctx?.bot.category === 'ai_agent' ||
    ctx?.bot.category === 'search_crawler';

  if (!discoveryLane) return null;

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{
        __html: JSON.stringify({
          '@context': 'https://schema.org',
          '@type': 'FAQPage',
          mainEntity: faqs.map((faq) => ({
            '@type': 'Question',
            name: faq.question,
            acceptedAnswer: { '@type': 'Answer', text: faq.answer },
          })),
        }),
      }}
    />
  );
}

For crawlers and answer agents that do not execute JavaScript, use Edge capture. This component is the enhancement path for machine-readable clients that do run page code.

Automation QA recipe

'use client';

import { useEffect } from 'react';
import { useVisitor } from '@clickstreamhq/react';

export function QaLaneMarker() {
  const { ctx } = useVisitor();

  useEffect(() => {
    if (ctx?.bot.category !== 'automation') return;
    window.dispatchEvent(new CustomEvent('clickstream:qa-coverage', {
      detail: {
        page: location.pathname,
        sessionId: ctx.session.sessionId,
        score: ctx.bot.score,
      },
    }));
  }, [ctx]);

  return null;
}

useIdentify(): (email: string) => Promise<void>

Returns a callback that hashes the email client-side (SHA-256 + MD5) and sends an identify event.

'use client';

import { useIdentify } from '@clickstreamhq/react';

export function LoginForm() {
  const identify = useIdentify();
  return (
    <form onSubmit={async (e) => {
      e.preventDefault();
      const email = (e.currentTarget.elements.namedItem('email') as HTMLInputElement).value;
      await identify(email);
    }}>
      <input name="email" type="email" />
      <button type="submit">Sign in</button>
    </form>
  );
}

useTrack(): (event: TrackEventInput) => void

Returns a callback that fires a custom event.

'use client';

import { useTrack } from '@clickstreamhq/react';

export function FilterToggle() {
  const track = useTrack();
  return (
    <button onClick={() => track({ name: 'docs_filter_changed', category: 'interaction' })}>
      Apply filter
    </button>
  );
}

TrackEventInput matches CustomEvent from packages/shared-types (see Event schema).

useClickStream(): ClickStreamState

Low-level meta-hook. Returns { configured, error }. Prefer useVisitor, useIdentify, and useTrack for component code.

'use client';

import { useClickStream } from '@clickstreamhq/react';

export function DebugPanel() {
  const { configured, error } = useClickStream();
  if (!configured) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return <p>ClickStream is configured.</p>;
}

Throws if called outside a <ClickStreamProvider>.

Provider props

PropTypeDefaultNotes
apiKeystringrequiredClickStream API key.
endpointstringrequired in practiceYour first-party tracking domain.
pollIntervalMsnumber2000How often useVisitor polls the signals endpoint. Floor 1000.

Server-side rendering

The React adapter is client-only ('use client'). Import it from a Client Component. For Next.js Server Components + Route Handlers, use @clickstreamhq/next — it exposes getServerVisitor() which reads the first-party cookie server-side.

The two adapters are designed to coexist: the Next middleware pre-fetches the VisitorContext into a request header, the server helper returns a snapshot at render time, and the React provider keeps the client in sync for interaction events.

Bundle impact

@clickstreamhq/react itself is small and imports @clickstreamhq/signals. The event-tracking pixel is loaded separately from your first-party tracking domain through the script tag or @clickstreamhq/sdk installer. That split keeps the React hooks thin and keeps the browser tracker centrally updated.

Migration

If you were manually adding ClickStream in React, the migration is mechanical:

+ import { installClickstreamPixel } from '@clickstreamhq/sdk';
+ import { ClickStreamProvider } from '@clickstreamhq/react';
+ useEffect(() => {
+   installClickstreamPixel({ apiKey, endpoint: 'https://t.example.com' });
+ }, []);
+ <ClickStreamProvider apiKey={…} endpoint="https://t.example.com">…</ClickStreamProvider>

+ const track = useTrack();
+ track({ name: 'help_panel_opened' });

See also