@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:
- Human interaction: show human-only UI or route to the right in-page view.
- AI/search coverage: expose structured content when a machine-readable visitor runs JavaScript.
- Automation/QA: tag scripted runs without counting them as human demand.
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
| Prop | Type | Default | Notes |
|---|---|---|---|
apiKey | string | required | ClickStream API key. |
endpoint | string | required in practice | Your first-party tracking domain. |
pollIntervalMs | number | 2000 | How 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
- Next.js adapter — Server Component + middleware support
- Signals API — underlying
VisitorContextshape - Install — full install matrix