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.
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 (byeventId) may be sent more than once due to retry logic orsendBeacon+fetchfallback. - 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
timestampascending. - 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.
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():
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.
import { memoryAdapter } from "react-clickmap";
const adapter = memoryAdapter();
// Development helpers:
adapter.inspect(); // CaptureEvent[] — view all stored events
adapter.clear(); // void — wipe all eventsCapabilities: { supportsAggregation: false, supportsRetention: false, supportsIdempotency: false }
`localStorageAdapter(options?)`
Persists events to browser localStorage. Falls back to memoryAdapter when localStorage is unavailable (e.g., SSR).
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.
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:
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`
npm install @react-clickmap/postgresimport { 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:
import { rollupDaily } from "@react-clickmap/postgres";
await rollupDaily(sql); // previous complete UTC day
await rollupDaily(sql, { day: "2026-07-01" }); // a specific daySee the Persistence Guide for the full SQL schema and setup instructions.
`createSupabaseAdapter(options)` — `@react-clickmap/supabase`
npm install @react-clickmap/supabaseimport { 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`
npm install @react-clickmap/nextimport { 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:
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>;
}| Return | Type | Description | |
|---|---|---|---|
data | CaptureEvent[] | Loaded events (empty array while loading) | |
isLoading | boolean | Whether a load is in progress | |
error | `Error \ | null` | Error from the most recent load |
reload | () => void | Manually trigger a re-fetch |