react-clickmap
Docs/API Reference

Adapters API

Every adapter implements this interface. `save` and `load` are required; `deleteEvents` and `loadAggregated` are optional.

`ClickmapAdapter` interface

Every adapter implements this interface. save and load are required; deleteEvents and loadAggregated are optional.

TypeScript
interface ClickmapAdapter {
  capabilities?: AdapterCapabilities;
  save(events: CaptureEvent[]): Promise<void>;
  load(query: HeatmapQuery): Promise<CaptureEvent[]>;
  deleteEvents?(query: HeatmapQuery): Promise<number>;
  loadAggregated?(query: HeatmapQuery): Promise<AggregatedHeatmapPayload>;
}

interface AdapterCapabilities {
  supportsAggregation: boolean;   // Has loadAggregated()
  supportsRetention: boolean;     // Can delete old data
  supportsIdempotency: boolean;   // Deduplicates by eventId
}

`save(events)`

Called by the event batcher when it flushes. Receives an array of CaptureEvent objects.

  • Must be idempotent if the adapter declares supportsIdempotency. The same event (by eventId) may be sent more than once due to retry logic or sendBeacon + fetch fallback.
  • The array may be empty — adapters should handle this gracefully (no-op).

`load(query)`

Called by the <Heatmap> component and the useHeatmapData() hook to fetch events for rendering.

  • Should return events sorted by timestamp ascending.
  • Should respect all filters in the HeatmapQuery (see below).

`deleteEvents(query)` (optional)

Used for GDPR/CCPA compliance and data retention. Returns the number of deleted events.

  • Should require at least one filter to prevent accidental full-table deletes.

`loadAggregated(query)` (optional)

Returns pre-binned coordinate data for large datasets. This avoids loading millions of raw events to the client.

TypeScript
interface AggregatedHeatmapPayload {
  width: number;      // viewport width used for rendering
  height: number;     // viewport height used for rendering
  bins: AggregatedBin[];
  totalEvents: number;
}

interface AggregatedBin {
  x: number;    // percentage (0–100) in the requested coordinate space
  y: number;    // percentage (0–100) in the requested coordinate space
  value: number; // weight (rage clicks count as 2)
}

Pass coordinateSpace: "document" in the query to aggregate the full-page (document-relative) heatmap; the default is "viewport".

`HeatmapQuery`

The query object passed to load(), deleteEvents(), and loadAggregated():

TypeScript
interface HeatmapQuery {
  page?: string;                                    // pathname filter (e.g., "/pricing")
  routeKey?: string;                                // SPA route key
  sessionId?: string;                               // session filter
  projectId?: string;                               // project filter
  userId?: string;                                   // user filter
  device?: "all" | "desktop" | "tablet" | "mobile"; // device filter
  types?: CaptureType[];                             // event type filter
  from?: number;                                     // timestamp >= (ms since epoch)
  to?: number;                                       // timestamp <= (ms since epoch)
  limit?: number;                                    // max results
  coordinateSpace?: "viewport" | "document";        // frame for loadAggregated() (default "viewport")
}

Built-in adapters

`memoryAdapter(seedEvents?)`

In-memory event storage. Events are lost on page refresh.

TypeScript
import { memoryAdapter } from "react-clickmap";

const adapter = memoryAdapter();

// Development helpers:
adapter.inspect();  // CaptureEvent[] — view all stored events
adapter.clear();    // void — wipe all events

Capabilities: { supportsAggregation: false, supportsRetention: false, supportsIdempotency: false }

`localStorageAdapter(options?)`

Persists events to browser localStorage. Falls back to memoryAdapter when localStorage is unavailable (e.g., SSR).

TypeScript
import { localStorageAdapter } from "react-clickmap";

const adapter = localStorageAdapter({
  key: "my_events",    // localStorage key (default: "clickmap_events")
  maxEvents: 5000,     // cap stored events
});

Capabilities: { supportsAggregation: false, supportsRetention: false, supportsIdempotency: false }

`fetchAdapter(options)`

HTTP transport adapter. Sends events to your API endpoint. This is the standard production adapter on the client side.

TypeScript
import { fetchAdapter } from "react-clickmap";

const adapter = fetchAdapter({
  endpoint: "/api/clickmap",
  headers: { Authorization: "Bearer token" },
  preferBeacon: true,       // use sendBeacon on page exit (default: true)
  keepalive: true,          // use fetch keepalive (default: true)
  maxPayloadBytes: 65536,   // split batches above this size
});

Capabilities: { supportsAggregation: false, supportsRetention: false, supportsIdempotency: false }

Reliability: Attempts sendBeacon first for page-exit events, falls back to fetch with keepalive, and automatically splits payloads that exceed the keepalive byte limit. sendBeacon cannot carry custom headers — as soon as headers is set, save() always uses fetch(..., { keepalive: true }) instead of sendBeacon, even on page exit, so configured headers (e.g. an Authorization token) are never silently dropped.

`createAdapter(implementation)`

Convenience wrapper for creating a custom adapter from a plain object:

TypeScript
import { createAdapter } from "react-clickmap";

const adapter = createAdapter({
  save: async (events) => { /* ... */ },
  load: async (query) => { /* ... */ },
  deleteEvents: async (query) => { /* ... */ },
});

Add-on adapters

`createPostgresAdapter(options)` — `@react-clickmap/postgres`

Shell
npm install @react-clickmap/postgres
TypeScript
import { createPostgresAdapter } from "@react-clickmap/postgres";

const adapter = createPostgresAdapter({
  sql: pool,                    // any { query(text, params) => { rows, rowCount } }
  tableName: "clickmap_events", // default
  preferDailyBins: true,        // default — read daily rollups when possible
});

Capabilities: { supportsAggregation: true, supportsRetention: true, supportsIdempotency: true }

Uses ON CONFLICT (event_id) DO NOTHING for idempotent inserts. Supports server-side coordinate binning via loadAggregated() for both viewport and document coordinate spaces.

Apply migrations sql/0001_init.sql then sql/0002_document_coordinates.sql (the latter adds document-coordinate columns and coordinate-space-aware daily bins). Both are additive.

Daily rollups. rollupDaily(sql, { day, routeKey }) pre-aggregates a UTC day of raw events into clickmap_heatmap_bins_daily and clickmap_element_clicks_daily (idempotent — safe to re-run). Schedule it once per day (cron / pg_cron), and loadAggregated() will serve day-aligned ranges from those tables instead of scanning raw events:

TypeScript
import { rollupDaily } from "@react-clickmap/postgres";

await rollupDaily(sql);                              // previous complete UTC day
await rollupDaily(sql, { day: "2026-07-01" });       // a specific day

See the Persistence Guide for the full SQL schema and setup instructions.

`createSupabaseAdapter(options)` — `@react-clickmap/supabase`

Shell
npm install @react-clickmap/supabase
TypeScript
import { createSupabaseAdapter } from "@react-clickmap/supabase";

const adapter = createSupabaseAdapter({
  url: "https://your-project.supabase.co",
  anonKey: "your-anon-key",
  table: "clickmap_events",  // default
  schema: "public",          // default
});

Capabilities: { supportsAggregation: false, supportsRetention: true, supportsIdempotency: true }

Communicates via the Supabase REST API (PostgREST). Uses the Prefer: resolution=ignore-duplicates header for idempotent inserts, and chunks large save() batches to stay under PostgREST's request size limits. loadAggregated() is implemented, but it fetches raw rows and reduces them in JS rather than binning in SQL — supportsAggregation reports false so callers don't assume the server does the heavy lifting. Use the Postgres adapter for real server-side aggregation.

`createNextFetchAdapter(options)` — `@react-clickmap/next`

Shell
npm install @react-clickmap/next
TypeScript
import { createNextFetchAdapter } from "@react-clickmap/next";

const adapter = createNextFetchAdapter({
  endpoint: "/api/clickmap",  // default
});

Convenience wrapper around fetchAdapter with Next.js defaults. See the Next.js guide for usage with createClickmapRouteHandlers.

`useHeatmapData` hook

React hook that loads events from an adapter:

TSX
import { useHeatmapData } from "react-clickmap";

function MyDashboard() {
  const { data, isLoading, error, reload } = useHeatmapData(adapter, {
    page: "/pricing",
    device: "desktop",
    from: Date.now() - 7 * 24 * 60 * 60 * 1000,  // last 7 days
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return <p>{data.length} events loaded</p>;
}
ReturnTypeDescription
dataCaptureEvent[]Loaded events (empty array while loading)
isLoadingbooleanWhether a load is in progress
error`Error \null`Error from the most recent load
reload() => voidManually trigger a re-fetch