@clickstream/react

Official React adapter. Wraps @clickstream/sdk + @clickstream/signals in React primitives so components can read visitor state and emit events without touching the underlying tracker instance.

Peer deps: react ^18 || ^19. Runtime deps: @clickstream/sdk, @clickstream/signals.

Install

pnpm add @clickstream/react @clickstream/sdk @clickstream/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 { ClickStreamProvider } from '@clickstream/react';

export function Providers({ children }: { children: React.ReactNode }) {
  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.

Hooks

useVisitor(): VisitorContext | null

Returns the current VisitorContext — bot classification, identity status, 11 behavioral scores, session + device summary. null while the signals endpoint warms up or when no cookie is set.

'use client';

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

export function Pricing() {
  const visitor = useVisitor();

  if (!visitor) return <DefaultPricing />;
  if (visitor.scores.intent >= 70) return <HighIntentOffer />;
  if (visitor.scores.frustration >= 60) return <SupportPromo />;
  return <DefaultPricing />;
}

The hook re-renders on every VisitorContext update — internally a 2-second poll of the signals endpoint. Pause the poll with <ClickStreamProvider pollIntervalMs={0}> if you want one-shot reads only.

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 '@clickstream/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 '@clickstream/react';

export function UpgradeCta() {
  const track = useTrack();
  return (
    <button onClick={() => track({ name: 'upgrade_clicked', category: 'conversion' })}>
      Upgrade
    </button>
  );
}

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

useClickStream(): ClickStreamState

Low-level meta-hook. Returns { tracker, configured, error }. Prefer the narrower hooks for component code; reach for this one only when you need the raw tracker instance.

'use client';

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

export function DebugPanel() {
  const { tracker, configured, error } = useClickStream();
  if (!configured) return <p>Loading…</p>;
  if (error) return <p>Error: {error.message}</p>;
  return <p>Visitor id: {tracker?.visitorId}</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 250.
debugbooleanfalseLog init / flush failures.

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 @clickstream/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

@clickstream/react itself is ~4 KB gzipped. It imports @clickstream/signals (~6 KB gzipped) and @clickstream/sdk (42 KB gzipped full-featured, or 2 KB via /core). The React layer adds minimal overhead; most of the weight is the underlying tracker.

Migration

If you were using @clickstream/sdk directly in React, the migration is mechanical:

- import { IdentityTracker } from '@clickstream/sdk';
- const tracker = new IdentityTracker({ apiKey, endpoint });
- useEffect(() => { tracker.init(); return () => tracker.destroy(); }, []);
+ // In your root layout:
+ import { ClickStreamProvider } from '@clickstream/react';
+ <ClickStreamProvider apiKey={…} endpoint="https://t.example.com">…</ClickStreamProvider>

- tracker.trackEvent({ name: 'signup' });
+ const track = useTrack();
+ track({ name: 'signup' });

See also